Introduzione
In JavaScript le funzioni sono cittadini di prima classe: possono essere assegnate a variabili, passate come argomenti, restituite da altre funzioni e memorizzate in strutture dati. Questa caratteristica permette di costruire componenti molto flessibili e riutilizzabili.
In questo tutorial utilizzeremo questi concetti per creare un rate limiter, cioè una funzione che limita il numero di operazioni eseguibili in un determinato intervallo di tempo. È un meccanismo utile, per esempio, quando vogliamo evitare di inviare troppe richieste a un’API, proteggere un servizio da chiamate ravvicinate oppure controllare l’uso di una funzione costosa.
Il componente sarà costruito con una closure. La funzione esterna conserverà alcune informazioni private, come il numero di operazioni già eseguite e il momento in cui il limite verrà azzerato. La funzione restituita potrà essere usata normalmente, senza esporre direttamente lo stato interno.
Codice completo
/**
* Crea una funzione che limita il numero di esecuzioni.
*
* @param {number} maxCalls - Numero massimo di chiamate consentite.
* @param {number} intervalMs - Durata della finestra temporale in millisecondi.
* @returns {Function} Una funzione rate-limited.
*/
function createRateLimiter(maxCalls, intervalMs) {
if (!Number.isInteger(maxCalls) || maxCalls <= 0) {
throw new Error("maxCalls deve essere un intero positivo");
}
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new Error("intervalMs deve essere un numero positivo");
}
// Stato privato, accessibile solo dalla closure
let callsUsed = 0;
let windowStartedAt = Date.now();
return function limitedFunction(callback, ...args) {
if (typeof callback !== "function") {
throw new TypeError("callback deve essere una funzione");
}
const now = Date.now();
// Se la finestra temporale è terminata, si ricomincia da zero
if (now - windowStartedAt >= intervalMs) {
callsUsed = 0;
windowStartedAt = now;
}
// Il limite è stato raggiunto
if (callsUsed >= maxCalls) {
return {
executed: false,
result: undefined,
remaining: 0
};
}
callsUsed++;
return {
executed: true,
result: callback(...args),
remaining: maxCalls - callsUsed
};
};
}
// Funzione di prima classe: viene passata come argomento
function saveMessage(message) {
return `Messaggio salvato: ${message}`;
}
// Permette al massimo 2 operazioni ogni 5 secondi
const limitedSave = createRateLimiter(2, 5000);
console.log(limitedSave(saveMessage, "Primo messaggio"));
// { executed: true, result: "...", remaining: 1 }
console.log(limitedSave(saveMessage, "Secondo messaggio"));
// { executed: true, result: "...", remaining: 0 }
console.log(limitedSave(saveMessage, "Terzo messaggio"));
// { executed: false, result: undefined, remaining: 0 }
Spiegazione
La funzione esterna
createRateLimiter è una factory: riceve una configurazione e restituisce una nuova funzione personalizzata. I parametri maxCalls e intervalMs descrivono il comportamento del limitatore.
All’interno vengono dichiarate due variabili:
callsUsed, che indica quante chiamate sono state effettuate nella finestra corrente;windowStartedAt, che memorizza l’istante di inizio della finestra temporale.
Quando la funzione esterna termina, normalmente le sue variabili locali non sarebbero più accessibili. Tuttavia, la funzione restituita continua a riferirsi a esse. Questo collegamento persistente è una closure.
La funzione restituita
La funzione limitedFunction riceve una callback e i suoi argomenti. La callback può essere una funzione dichiarata con function, una arrow function o una funzione ottenuta da un’altra parte dell’applicazione.
Questa è una dimostrazione concreta del fatto che le funzioni sono valori. Il rate limiter non deve conoscere il contenuto della callback: si limita a decidere se eseguirla oppure no.
La finestra temporale
Prima di verificare il limite, il codice calcola il tempo trascorso dall’inizio della finestra. Se questo valore è maggiore o uguale a intervalMs, il contatore viene azzerato e parte una nuova finestra.
Quando il limite viene raggiunto, la callback non viene eseguita. L’oggetto restituito contiene comunque informazioni utili: executed indica se l’operazione è stata eseguita, mentre remaining mostra quante chiamate restano disponibili.
Più istanze indipendenti
const apiLimiter = createRateLimiter(3, 10000);
const reportLimiter = createRateLimiter(1, 60000);
const loadUsers = () => "Utenti caricati";
const generateReport = () => "Report generato";
console.log(apiLimiter(loadUsers));
console.log(reportLimiter(generateReport));
// Ogni limitatore possiede una closure e uno stato separato.
Le due istanze non condividono callsUsed né windowStartedAt. Questo isolamento è uno dei principali vantaggi delle closures.
Best practice
- Validare gli argomenti: verificare configurazioni errate rende il componente più prevedibile.
- Non esporre lo stato interno: le variabili della closure devono essere modificate solo attraverso l’API pubblica.
- Restituire informazioni chiare: un oggetto con
executederemainingè più utile di un semplice valore booleano. - Separare il controllo dall’operazione: il limiter decide se chiamare la callback, ma non deve conoscere la logica dell’operazione.
- Considerare il contesto asincrono: questo esempio controlla l’avvio delle operazioni, non il loro completamento. Per API asincrone si può restituire una Promise oppure gestire esplicitamente gli errori.
- Usare nomi descrittivi: nomi come
callsUsedewindowStartedAtrendono immediata la lettura del codice.
Riepilogo
Abbiamo creato un rate limiter riutilizzabile sfruttando tre concetti fondamentali di JavaScript:
- le funzioni possono essere passate come argomenti;
- una funzione può restituire un’altra funzione;
- una closure conserva l’accesso alle variabili della funzione esterna.
Il risultato è un componente configurabile, con stato privato e più istanze indipendenti. Lo stesso schema può essere adattato per limitare chiamate a servizi web, operazioni di scrittura, invii di notifiche o funzioni computazionalmente costose.
