UNPKG

@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
"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