mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
a16ec24cd7
commit
be3a196744
1 file changed
+56
-62
+56
-62
@@ -143,19 +143,27 @@ async function fetchAllPages<T>(
|
||||
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=<code>&limit=<n> → tous les rankings du fournisseur
|
||||
* Récupère le ranking réseau pour les articles en 1 seul appel :
|
||||
* GET /api/ranking?limit=<N> — 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<RankingResult> {
|
||||
const rankings = new Map<string, FfRanking>();
|
||||
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<string, string | null>[] = 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<string, string | null>[]) {
|
||||
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 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user