From be3a1967446353cdb2cf3e089719e41259b5ad82 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 19 Mar 2026 20:58:03 +0100 Subject: [PATCH] perf: parallel article pagination + single-call full ranking fetch Article pagination: - fetchAllPages fallback now fetches 10 pages in parallel instead of sequentially. Stops as soon as any page returns < pageSize items. For S053 (30 pages), reduces sequential time by ~10x. Ranking: - getRankingByArticles now fetches the entire network ranking in one call (GET /api/ranking?limit=30000) and matches by codein. Replaces foucentrale bulk (which returned 0) and per-article fallback (which would be 8889 calls for S053). - totalRankedProducts = actual number of entries returned by the API. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/api-ff-client.ts | 118 +++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 62 deletions(-) diff --git a/src/lib/api-ff-client.ts b/src/lib/api-ff-client.ts index 9013b68..2328f43 100644 --- a/src/lib/api-ff-client.ts +++ b/src/lib/api-ff-client.ts @@ -143,19 +143,27 @@ async function fetchAllPages( return all; } - // Fallback séquentiel (pas de total dans la réponse) + // Fallback parallèle sans total : fetch 10 pages à la fois, arrêt dès qu'une page est incomplète const all = [...items1]; - let page = 2; + let startPage = 2; while (true) { - const url = buildUrl(page); - console.log(`[api-ff] GET ${url}`); - const res = await fetch(url, { cache: "no-store" }); - if (!res.ok) break; - const data = await res.json(); - const items = extractList(data) as T[]; - all.push(...items); - if (items.length < pageSize) break; - page++; + const chunk = Array.from({ length: 10 }, (_, i) => startPage + i); + const results = await Promise.all(chunk.map(async (p) => { + const url = buildUrl(p); + console.log(`[api-ff] GET ${url}`); + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) return { p, items: [] as T[] }; + const data = await res.json(); + return { p, items: extractList(data) as T[] }; + })); + results.sort((a, b) => a.p - b.p); + let done = false; + for (const { items } of results) { + all.push(...items); + if (items.length < pageSize) { done = true; break; } + } + if (done) break; + startPage += 10; } return all; } @@ -487,68 +495,54 @@ export interface RankingResult { } /** - * Récupère le ranking (réseau + magasin) pour un fournisseur en 2 appels : - * 1. GET /api/ranking?limit=1 → count total de produits classés - * 2. GET /api/ranking?foucentrale=&limit= → tous les rankings du fournisseur + * Récupère le ranking réseau pour les articles en 1 seul appel : + * GET /api/ranking?limit= — récupère tout le classement réseau, puis filtre par codein. * - * Fallback par articles individuels si foucentrale n'est pas disponible. + * Fallback par article individuel si le bulk échoue. */ export async function getRankingByArticles( articles: { codein: string; gtin?: string }[], - codeFournisseur?: string, + _codeFournisseur?: string, ): Promise { const rankings = new Map(); if (articles.length === 0) return { rankings, totalRankedProducts: 0 }; - // 1. Nombre total de produits classés - let totalRankedProducts = 0; + const articleCodeins = new Set(articles.map(a => a.codein)); + + // Stratégie : télécharger tout le classement réseau en 1 appel, puis matcher par codein + // Le nombre d'articles retournés = totalRankedProducts réel + const RANKING_FULL_LIMIT = 30000; try { - const countRes = await fetch(`${FF_API_BASE}/api/ranking?limit=1`, { cache: "no-store" }); - if (countRes.ok) { - const countData = await countRes.json(); - totalRankedProducts = countData?.count ?? 0; - console.log(`[api-ff] Ranking total produits classés : ${totalRankedProducts}`); + const url = `${FF_API_BASE}/api/ranking?limit=${RANKING_FULL_LIMIT}`; + const res = await fetch(url, { cache: "no-store" }); + if (res.ok) { + const data = await res.json(); + const rankingList: Record[] = data?.ranking ?? []; + const totalRankedProducts = rankingList.length; + console.log(`[api-ff] Ranking full fetch: ${rankingList.length} produits classés dans le réseau`); + + for (const entry of rankingList) { + const codein = entry.codein; + if (!codein || !articleCodeins.has(codein)) continue; + rankings.set(codein, { + ranking_ca: parseRankNum(entry.ranking_ca), + ranking_qte: parseRankNum(entry.ranking_qte), + ranking_mag_ca: parseRankNum(entry.ranking_mag_ca), + ranking_mag_qte: parseRankNum(entry.ranking_mag_qte), + ranking_mag_marge: parseRankNum(entry.ranking_mag_marge), + pv_calcule: parseRankNum(entry.pv_calcule), + pv_mag: parseRankNum(entry.pv_mag), + pv_cen: parseRankNum(entry.pv_cen), + }); + } + console.log(`[api-ff] Ranking: ${rankings.size}/${articles.length} articles du fournisseur classés`); + return { rankings, totalRankedProducts }; } } catch (err) { - console.warn(`[api-ff] Failed to get ranking total count:`, err); + console.warn(`[api-ff] Full ranking fetch failed, falling back to per-article:`, err); } - // 2. Bulk fetch par fournisseur (1 seul appel au lieu de N) - if (codeFournisseur) { - try { - const limit = Math.max(articles.length + 50, 500); - const url = `${FF_API_BASE}/api/ranking?foucentrale=${encodeURIComponent(codeFournisseur)}&limit=${limit}`; - const res = await fetch(url, { cache: "no-store" }); - if (res.ok) { - const data = await res.json(); - const rankingList: unknown[] = data?.ranking ?? []; - console.log(`[api-ff] Ranking bulk: ${rankingList.length} entrées pour fournisseur ${codeFournisseur}`); - if (rankingList.length > 0) { - console.log(`[api-ff] Ranking sample keys: ${Object.keys(rankingList[0] as object).join(",")}`); - } - for (const entry of rankingList as Record[]) { - const codein = entry.codein; - if (!codein) continue; - rankings.set(codein, { - ranking_ca: parseRankNum(entry.ranking_ca), - ranking_qte: parseRankNum(entry.ranking_qte), - ranking_mag_ca: parseRankNum(entry.ranking_mag_ca), - ranking_mag_qte: parseRankNum(entry.ranking_mag_qte), - ranking_mag_marge: parseRankNum(entry.ranking_mag_marge), - pv_calcule: parseRankNum(entry.pv_calcule), - pv_mag: parseRankNum(entry.pv_mag), - pv_cen: parseRankNum(entry.pv_cen), - }); - } - console.log(`[api-ff] Ranking: ${rankings.size}/${articles.length} articles enrichis`); - return { rankings, totalRankedProducts }; - } - } catch (err) { - console.warn(`[api-ff] Bulk ranking failed, falling back to per-article:`, err); - } - } - - // Fallback : 1 appel par article (si pas de codeFournisseur ou si bulk échoue) + // Fallback : 1 appel par article const batchSize = 20; for (let i = 0; i < articles.length; i += batchSize) { const batch = articles.slice(i, i + batchSize); @@ -579,8 +573,8 @@ export async function getRankingByArticles( ); } - console.log(`[api-ff] Ranking: ${rankings.size}/${articles.length} articles enrichis`); - return { rankings, totalRankedProducts }; + console.log(`[api-ff] Ranking fallback: ${rankings.size}/${articles.length} articles enrichis`); + return { rankings, totalRankedProducts: 0 }; } // ---------------------------------------------------------------------------