Classifica mensile
Scoring incrementale on-write e chiusura giornaliera schedulata, con floor a 0 e vincitore in evidenza.
Obiettivo
Premiare la costanza, non il volume. Un autore che pubblica tutti i giorni — anche una poesia soltanto — finisce avanti a uno che ne scrive dieci in un pomeriggio e poi sparisce per un mese.
Schema Firestore
leaderboards/
{YYYY-MM}/ ← un documento per mese (chiave mese)
entries/
{uid}/ ← un documento per autore
score, displayName, photoURL, isPremium,
lastCreditedDay, daysCredited, daysMissed
Lo sharding per mese evita che una collection cresca all’infinito: quando il mese si chiude, quel sottoinsieme resta come storico e il successivo parte da zero.
Scoring: due momenti distinti
+1 on-write
Il punto positivo è applicato dal trigger updateLeaderboardOnPoem su poems/{poemId}, ma solo se:
- la poesia non è bozza né privata,
moderationPassed === true.
Questo significa che il punto arriva nel momento in cui il contenuto diventa davvero pubblico — non al momento della submission. Il campo lastCreditedDay impedisce doppi punti nello stesso giorno.
−3 on-schedule
Una scheduled function gira ogni giorno a 00:05 Europe/Rome (closeDailyLeaderboard): guarda chi non ha pubblicato il giorno precedente e applica −3. Il trigger on-write non sa chi è “assente”, quindi la penalità va per forza fatta batch.
Lo score è floored a 0: non salviamo mai valori negativi. Un nuovo utente o un utente dormiente non si scava un buco infinito da cui non uscirebbe mai.
Concorrenza e limiti
La chiusura giornaliera itera le entries in batch da 20 con Promise.all per non saturare Firestore — una mensilità può contenere centinaia di autori e gli aggiornamenti sono serializzabili per documento. Timeout della function fissato a 5 minuti, con retry singolo in caso di errore transitorio.
Vincitore del mese
Nello stesso job che chiude il giorno, se stiamo attraversando la mezzanotte del primo del mese, la function determina il vincitore del mese precedente:
- Ordina le entries per
scoredecrescente. - Scrive l’UID del primo in
leaderboards/{monthKey}/winner. - La UI mostra il vincitore in cima al feed pubblico per i primi 7 giorni del mese successivo, leggendo da quel documento.
Lo storico completo resta interrogabile per costruire pagine “hall of fame” o esportare dati per stagioni tematiche.
Denormalizzazione profilo
Le entries duplicano displayName, photoURL, isPremium invece di joinare su users/{uid} lato client. Quando uno di questi campi cambia, syncUserPublicFields propaga la modifica a tutte le entries storiche dell’utente (oltre che a poesie e commenti). Il costo in scritture è ripagato dal fatto che la classifica diventa leggibile con una query singola.