mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 22:18:08 +02:00
perf: lot 1 — chargements plus rapides et corrections, sans changement visuel
Grille (navigateur) - « Rafraîchir » ne passe plus par l'URL : le paramètre _refresh restait mémorisé et chaque retour par le menu forçait un recalcul serveur complet (migration du store pour nettoyer les valeurs déjà enregistrées). - Lignes reçues regroupées toutes les 300 ms au lieu de reconstruire tout le tableau à chaque paquet de 150 (chargement quadratique). - Sélection indexée par code article (getRowId) : après un filtre, l'action groupée visait d'autres produits. Seules les lignes affichées comptent. - Recherche : un passage par ligne au lieu d'un par colonne ; formateurs de nombres partagés (lib/format.ts) ; colonnes mensuelles mémorisées ; plus de transition-all sur les lignes positionnées par transform. - exceljs, jspdf et xlsx chargés au clic seulement. Grille (serveur) - Requêtes Qlik et snapshot lancées en parallèle de la phase SQL. - Cache mémoire borné (LRU) ; calcul en cours réutilisé même en forcé ; flux interrompu quand le client part. - Après un enregistrement, les gammes sont reportées dans le cache et dans grid_rows au lieu d'invalider (recalcul de ~40 s évité). Données et API - lib/ff-cache.ts : cache mémoire 30 min des lectures FF (stock, hit-parade, CA mensuel, fournisseurs, dernière réception, publicités), vidé en fin de synchro nocturne ; erreurs jamais gardées. - Pool PostgreSQL unique (globalThis), réglage sans workers parallèles posé une fois par connexion au lieu d'une transaction autour de chaque requête. - Délai maximal sur tous les appels à l'API FF. - Index session_snapshots ; commandes : plus de double rechargement ; synchro : saisies regroupées ; paramètres : configuration lue une fois ; journal de capture en ajout seul, rien de formaté sans capture ouverte. Corrections - Historique : l'échec de chargement s'affiche (au lieu de « Aucun snapshot »). - Sessions et validations enregistrent le magasin affiché, plus « TOTAL ». - Publicités : libellé du statut « passées ». Plan complet : docs/plan-optimisation-webui.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
35 files changed
+1010
-240
No files matched your search
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* CollectFlow — Cache mémoire des lectures « FF » (base miroir et API FF Nancy).
|
||||
*
|
||||
* Les tables FF (`mvtart`, `cube_stock`, `articles`…) ne sont recopiées qu'une
|
||||
* fois par nuit depuis SQL Server : recalculer à chaque visite les mêmes
|
||||
* agrégations lourdes (stock, hit-parade, CA mensuel…) coûtait plusieurs
|
||||
* secondes par page pour un résultat identique.
|
||||
*
|
||||
* Pourquoi pas `unstable_cache` : il sérialise en JSON (une `Map` devient `{}`,
|
||||
* une `Date` une chaîne) et ne peut pas être invalidé hors d'une requête — or
|
||||
* c'est la synchro nocturne, lancée par un minuteur, qui sait quand les données
|
||||
* changent. Ici :
|
||||
* - une durée de vie bornée (FF_CACHE_TTL_S, 30 min par défaut) ;
|
||||
* - les appels simultanés pour la même clé partagent une seule requête ;
|
||||
* - une erreur n'est jamais mise en cache ;
|
||||
* - `invalidateFFCache()` vide tout (appelé en fin de synchro nocturne).
|
||||
*/
|
||||
|
||||
import "server-only";
|
||||
|
||||
import { pgGetDerniereReceptionParFournisseur, pgGetFournisseurs } from "@/lib/pg-ff-client";
|
||||
|
||||
const TTL_MS = (Number(process.env.FF_CACHE_TTL_S) || 30 * 60) * 1000;
|
||||
/** Plafond d'entrées : les clés incluent des paramètres (dates, mois, magasin). */
|
||||
const MAX_ENTRIES = 100;
|
||||
|
||||
interface Entry {
|
||||
value?: unknown;
|
||||
expiresAt: number;
|
||||
pending?: Promise<unknown>;
|
||||
}
|
||||
|
||||
// Rangé dans globalThis : un seul cache par processus, même si ce module est
|
||||
// présent dans plusieurs lots compilés (routes, instrumentation).
|
||||
const globalForFFCache = globalThis as typeof globalThis & {
|
||||
__collectflowFFCache?: { store: Map<string, Entry>; generation: number };
|
||||
};
|
||||
const cache = (globalForFFCache.__collectflowFFCache ??= { store: new Map(), generation: 0 });
|
||||
const store = cache.store;
|
||||
|
||||
interface CachedOptions<T> {
|
||||
/** Durée de vie propre à cette clé, en millisecondes. */
|
||||
ttlMs?: number;
|
||||
/**
|
||||
* Ne garder le résultat que s'il passe ce test. Sert aux lectures qui
|
||||
* avalent leurs erreurs et renvoient une liste vide : sans ce garde-fou,
|
||||
* une panne passagère resterait affichée pendant toute la durée de vie.
|
||||
*/
|
||||
cacheIf?: (value: T) => boolean;
|
||||
}
|
||||
|
||||
export async function cachedFF<T>(key: string, load: () => Promise<T>, options: CachedOptions<T> = {}): Promise<T> {
|
||||
const now = Date.now();
|
||||
const entry = store.get(key);
|
||||
if (entry && "value" in entry && entry.expiresAt > now) return entry.value as T;
|
||||
if (entry?.pending) return entry.pending as Promise<T>;
|
||||
|
||||
// Une invalidation pendant le chargement rend son résultat suspect : il est
|
||||
// alors renvoyé à l'appelant, mais pas conservé.
|
||||
const generation = cache.generation;
|
||||
const pending = load().then(
|
||||
(value) => {
|
||||
if (generation !== cache.generation) return value;
|
||||
if (options.cacheIf && !options.cacheIf(value)) {
|
||||
store.delete(key);
|
||||
} else {
|
||||
store.delete(key); // réinsertion en fin : ordre d'éviction = ancienneté
|
||||
store.set(key, { value, expiresAt: Date.now() + (options.ttlMs ?? TTL_MS) });
|
||||
while (store.size > MAX_ENTRIES) {
|
||||
const oldest = store.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
store.delete(oldest);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
if (generation === cache.generation) store.delete(key);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
// Une éventuelle valeur expirée est remplacée : seule la requête en cours compte.
|
||||
store.set(key, { expiresAt: 0, pending });
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** Vide tout le cache FF (fin de synchro nocturne, données fraîches). */
|
||||
export function invalidateFFCache(): void {
|
||||
cache.generation++;
|
||||
store.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lectures partagées par plusieurs pages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Référentiel fournisseurs (Grille, cadencier). Vide = erreur avalée, non gardé. */
|
||||
export function getFournisseursCached(): Promise<{ code: string; nom: string }[]> {
|
||||
return cachedFF("fournisseurs", () => pgGetFournisseurs(), { cacheIf: (rows) => rows.length > 0 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Dernière réception par fournisseur et magasin (cadencier). Agrège tout
|
||||
* l'historique de `mvtart` : de loin la lecture la plus lourde de la page
|
||||
* Commandes. Vide = erreur avalée, non gardé.
|
||||
*/
|
||||
export function getDerniereReceptionCached(): Promise<Map<string, string>> {
|
||||
return cachedFF("derniere-reception", () => pgGetDerniereReceptionParFournisseur(), {
|
||||
cacheIf: (map) => map.size > 0,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user