@checkfirst/nestjs-outlook
Version:
An opinionated NestJS module for Microsoft Outlook integration that provides easy access to Microsoft Graph API for emails, calendars, and more.
171 lines • 7.69 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var GraphRateLimiterService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphRateLimiterService = void 0;
const common_1 = require("@nestjs/common");
const schedule_1 = require("@nestjs/schedule");
const retry_util_1 = require("../../utils/retry.util");
let GraphRateLimiterService = GraphRateLimiterService_1 = class GraphRateLimiterService {
constructor() {
this.logger = new common_1.Logger(GraphRateLimiterService_1.name);
this.MAX_REQUESTS_PER_SECOND = 4;
this.MAX_REQUESTS_PER_10_MINUTES = 10000;
this.ONE_SECOND_MS = 1000;
this.TEN_MINUTES_MS = 10 * 60 * 1000;
this.INACTIVE_USER_THRESHOLD_MS = 30 * 60 * 1000;
this.userLimiters = new Map();
this.totalPermitsAcquired = 0;
this.totalWaitTime = 0;
this.cooldownCount = 0;
}
onModuleInit() {
this.logger.log(`GraphRateLimiterService initialized - Limits: ${this.MAX_REQUESTS_PER_SECOND} req/sec, ` +
`${this.MAX_REQUESTS_PER_10_MINUTES} req/10min per user`);
}
async acquirePermit(userId) {
if (!this.userLimiters.has(userId)) {
this.initializeLimiter(userId);
}
const limiter = this.userLimiters.get(userId);
if (!limiter) {
this.logger.warn(`[acquirePermit] No limiter found for user ${userId}, skipping acquire`);
return;
}
const startTime = Date.now();
let waitedOnce = false;
while (!this.canMakeRequest(userId)) {
if (!waitedOnce) {
waitedOnce = true;
this.logger.debug(`[acquirePermit] User ${userId} rate limited, entering wait loop`);
}
const oldestRequest = limiter.recentRequests[0];
const waitTime = oldestRequest
? this.ONE_SECOND_MS - (Date.now() - oldestRequest) + 50
: 100;
await (0, retry_util_1.delay)(Math.max(waitTime, 50));
}
const now = Date.now();
limiter.recentRequests.push(now);
limiter.tenMinuteWindow.push(now);
limiter.lastActivity = now;
this.totalPermitsAcquired++;
const totalWait = now - startTime;
if (totalWait > 50) {
this.totalWaitTime += totalWait;
this.logger.debug(`[acquirePermit] User ${userId} waited ${totalWait}ms for permit ` +
`(recent: ${limiter.recentRequests.length}/${this.MAX_REQUESTS_PER_SECOND}, ` +
`10min: ${limiter.tenMinuteWindow.length}/${this.MAX_REQUESTS_PER_10_MINUTES})`);
}
}
releasePermit(userId) {
if (!this.userLimiters.has(userId)) {
this.logger.warn(`[releasePermit] No limiter found for user ${userId}, skipping release`);
return;
}
}
handleRateLimitResponse(userId, retryAfterSeconds) {
if (!this.userLimiters.has(userId)) {
this.initializeLimiter(userId);
}
const limiter = this.userLimiters.get(userId);
if (!limiter) {
this.logger.warn(`[handleRateLimitResponse] No limiter found for user ${userId}, skipping cooldown`);
return;
}
const cooldownUntil = Date.now() + (retryAfterSeconds * 1000);
limiter.cooldownUntil = cooldownUntil;
this.cooldownCount++;
this.logger.warn(`[handleRateLimitResponse] User ${userId} hit 429 - cooldown for ${retryAfterSeconds}s ` +
`until ${new Date(cooldownUntil).toISOString()}`);
}
canMakeRequest(userId) {
const limiter = this.userLimiters.get(userId);
if (!limiter)
return true;
const now = Date.now();
limiter.recentRequests = limiter.recentRequests.filter(timestamp => now - timestamp < this.ONE_SECOND_MS);
limiter.tenMinuteWindow = limiter.tenMinuteWindow.filter(timestamp => now - timestamp < this.TEN_MINUTES_MS);
if (limiter.cooldownUntil && now < limiter.cooldownUntil) {
return false;
}
if (limiter.recentRequests.length >= this.MAX_REQUESTS_PER_SECOND) {
return false;
}
if (limiter.tenMinuteWindow.length >= this.MAX_REQUESTS_PER_10_MINUTES) {
this.logger.warn(`[canMakeRequest] User ${userId} hit 10-minute limit ` +
`(${limiter.tenMinuteWindow.length}/${this.MAX_REQUESTS_PER_10_MINUTES})`);
return false;
}
return true;
}
initializeLimiter(userId) {
this.userLimiters.set(userId, {
userId,
recentRequests: [],
tenMinuteWindow: [],
cooldownUntil: null,
lastActivity: Date.now(),
});
this.logger.debug(`[initializeLimiter] Created limiter for user ${userId}`);
}
cleanupInactiveUsers() {
const now = Date.now();
let cleaned = 0;
for (const [userId, limiter] of this.userLimiters.entries()) {
const inactiveDuration = now - limiter.lastActivity;
if (inactiveDuration > this.INACTIVE_USER_THRESHOLD_MS) {
this.userLimiters.delete(userId);
cleaned++;
}
}
if (cleaned > 0) {
this.logger.log(`[cleanupInactiveUsers] Cleaned up ${cleaned} inactive users ` +
`(${this.userLimiters.size} active limiters remaining)`);
}
}
getStats() {
return {
activeUsers: this.userLimiters.size,
totalPermitsAcquired: this.totalPermitsAcquired,
totalWaitTimeMs: this.totalWaitTime,
averageWaitTimeMs: this.totalPermitsAcquired > 0
? Math.round(this.totalWaitTime / this.totalPermitsAcquired)
: 0,
cooldownCount: this.cooldownCount,
};
}
getUserStats(userId) {
const limiter = this.userLimiters.get(userId);
if (!limiter) {
return null;
}
const now = Date.now();
return {
userId,
recentRequestCount: limiter.recentRequests.filter(t => now - t < this.ONE_SECOND_MS).length,
tenMinuteRequestCount: limiter.tenMinuteWindow.filter(t => now - t < this.TEN_MINUTES_MS).length,
cooldownUntil: limiter.cooldownUntil,
lastActivity: new Date(limiter.lastActivity).toISOString(),
};
}
};
exports.GraphRateLimiterService = GraphRateLimiterService;
__decorate([
(0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_5_MINUTES),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], GraphRateLimiterService.prototype, "cleanupInactiveUsers", null);
exports.GraphRateLimiterService = GraphRateLimiterService = GraphRateLimiterService_1 = __decorate([
(0, common_1.Injectable)()
], GraphRateLimiterService);
//# sourceMappingURL=graph-rate-limiter.service.js.map