Как обрабатывать превышение лимита вызовов функции в JavaScript и что возвращать
Что проверяют
Умеете ли вы написать обёртку, которая ограничивает вызовы, и продумать её контракт: что получает вызывающий код, когда лимит исчерпан. Следом просят ограничение по времени, например не больше пяти вызовов в минуту, или асинхронный вариант.
Как отвечать
Сначала уточните, какое ограничение нужно, это разные задачи:
- не больше N вызовов вообще: как
_.onceи_.before(n, fn)в lodash; - не больше N за окно времени: rate limiter;
- не чаще раза в интервал:
throttle, а «только после паузы» этоdebounce.
Реализация через замыкание, чтобы у каждой обёрнутой функции был свой счётчик:
function limit(fn, max, windowMs) {
const calls = [];
return function (...args) {
const now = Date.now();
while (calls.length && now - calls[0] >= windowMs) calls.shift();
if (calls.length >= max) {
throw new RateLimitError(windowMs - (now - calls[0]));
}
calls.push(now);
return fn.apply(this, args);
};
}Это скользящее окно по отметкам времени. Обёртка написана через function, а не стрелкой, чтобы пробросить this при вызове как метода.
Что возвращать при превышении, решает контракт. Главное выбрать один способ и держаться его:
- своё исключение с полем
retryAfter: явно, и его удобно ловить; - последний успешный результат: так ведёт себя
_.before, подходит для чистых функций; - для асинхронной функции: отклонённый
Promiseили очередь, которая выполнит вызов, когда окно освободится.
Если лимит на стороне сервера, фронт получает 429 Too Many Requests с заголовком Retry-After и повторяет запрос с экспоненциальной задержкой и случайным разбросом.
Где спотыкаются
Возвращают строку "Error: limit exceeded" из функции, которая обычно возвращает данные. Вызывающий код не отличит ошибку от результата.
Делают фиксированное окно со сбросом счётчика и не замечают, что на стыке двух окон проходит вдвое больше вызовов.
Хранят счётчик в переменной модуля. Все обёрнутые функции делят один лимит, а тесты влияют друг на друга.