Хранилище счётчика в Nest
Несколько экземпляров приложения на NodeJS не делят память одного процесса. Счётчик по умолчанию живёт локально, и лимит на одном узле не виден на другом, пока хранилище не вынести в общий слой.
Модуль принимает класс с интерфейсом
ThrottlerStorage из пакета
@nestjs/throttler и передаёт
его в поле storage при
ThrottlerModule.forRoot.
В документации Nest для Redis
указаны сторонние адаптеры,
а здесь покажем своё хранилище
в памяти с тем же контрактом,
чтобы счётчик можно было заменить
на общий позже.
Покажем на примере постов:
import {
ThrottlerStorage,
ThrottlerStorageRecord,
} from '@nestjs/throttler';
type CounterRow = {
hits: number;
resetAt: number;
blockedUntil: number;
};
export class PostCounterStorage implements ThrottlerStorage {
private map = new Map<string, CounterRow>();
async increment(
key: string,
ttl: number,
limit: number,
blockDuration: number,
throttlerName: string,
): Promise<ThrottlerStorageRecord> {
const storageKey = key + ':' + throttlerName;
const now = Date.now();
let row = this.map.get(storageKey);
if (!row || row.resetAt <= now) {
row = {
hits: 0,
resetAt: now + ttl,
blockedUntil: 0,
};
this.map.set(storageKey, row);
}
if (row.blockedUntil > now) {
return {
totalHits: row.hits,
timeToExpire: Math.ceil((row.resetAt - now) / 1000),
isBlocked: true,
timeToBlockExpire: Math.ceil(
(row.blockedUntil - now) / 1000,
),
};
}
row.hits += 1;
const isBlocked = row.hits > limit;
if (isBlocked && blockDuration > 0) {
row.blockedUntil = now + blockDuration;
}
const timeToExpire = Math.ceil((row.resetAt - now) / 1000);
const timeToBlockExpire =
row.blockedUntil > now
? Math.ceil((row.blockedUntil - now) / 1000)
: 0;
return {
totalHits: row.hits,
timeToExpire,
isBlocked,
timeToBlockExpire,
};
}
}
Передадим хранилище в настройку модуля:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { PostController } from './post.controller.js';
import { PostCounterStorage } from './post-counter.storage.js';
@Module({
imports: [
ThrottlerModule.forRoot({
throttlers: [
{
ttl: 60000,
limit: 10,
},
],
storage: new PostCounterStorage(),
}),
],
controllers: [PostController],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
Реализуйте своё хранилище счётчика
в памяти для API работников
и подключите его в корневом модуле
с WorkerController вместо
встроенного по умолчанию.