UNPKG

mpesajs

Version:

A Node.js SDK for seamless integration with M-Pesa payment gateway, providing easy-to-use methods for handling transactions, payments, and API interactions

170 lines 6.94 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RateLimiter = void 0; const axios_1 = require("axios"); const env_1 = require("./env"); /** * Gets configuration from environment variables with fallbacks * Parses string values from environment variables into appropriate types */ function getConfigFromEnv() { return { retryConfig: { maxRetries: parseInt((0, env_1.getEnvVar)('MPESAJS_MAX_RETRIES', '3'), 10), initialDelayMs: parseInt((0, env_1.getEnvVar)('MPESAJS_INITIAL_DELAY_MS', '1000'), 10), maxDelayMs: parseInt((0, env_1.getEnvVar)('MPESAJS_MAX_DELAY_MS', '10000'), 10), backoffFactor: parseInt((0, env_1.getEnvVar)('MPESAJS_BACKOFF_FACTOR', '2'), 10) }, rateLimitConfig: { maxConcurrent: parseInt((0, env_1.getEnvVar)('MPESAJS_MAX_CONCURRENT', '1000'), 10), timeWindowMs: parseInt((0, env_1.getEnvVar)('MPESAJS_TIME_WINDOW_MS', '60000'), 10) } }; } /** * RateLimiter class implements rate limiting and retry logic for API requests * Uses the Singleton pattern to ensure only one instance exists */ class RateLimiter { /** * Private constructor to prevent direct instantiation * Initializes default configurations for retry and rate limiting */ constructor() { this.queue = []; // Queue of pending requests this.activeRequests = 0; // Current number of executing requests this.requestTimestamps = []; // Timestamps of recent requests const config = getConfigFromEnv(); this.retryConfig = config.retryConfig; this.rateLimitConfig = config.rateLimitConfig; } /** * Gets the singleton instance of RateLimiter * Creates the instance if it doesn't exist */ static getInstance() { if (!RateLimiter.instance) { RateLimiter.instance = new RateLimiter(); } return RateLimiter.instance; } /** * Updates the retry configuration * Allows partial updates while maintaining existing values */ setRetryConfig(config) { this.retryConfig = Object.assign(Object.assign({}, this.retryConfig), config); } /** * Updates the rate limit configuration * Allows partial updates while maintaining existing values */ setRateLimitConfig(config) { this.rateLimitConfig = Object.assign(Object.assign({}, this.rateLimitConfig), config); } /** * Creates a promise that resolves after the specified delay * Used for implementing delays between retries */ delay(ms) { return __awaiter(this, void 0, void 0, function* () { return new Promise(resolve => setTimeout(resolve, ms)); }); } /** * Calculates the delay time for the next retry attempt using exponential backoff * Includes random jitter to prevent thundering herd problem */ calculateBackoff(attempt) { const delay = Math.min(this.retryConfig.initialDelayMs * Math.pow(this.retryConfig.backoffFactor, attempt), this.retryConfig.maxDelayMs); // Add 25% random jitter to prevent all retries happening simultaneously return delay * (0.75 + Math.random() * 0.5); } /** * Determines if an error should trigger a retry attempt * Retries on network errors and specific HTTP status codes */ shouldRetry(error) { var _a; if (error instanceof axios_1.AxiosError) { const status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status; return !status || // Network error (no status) status === 429 || // Too Many Requests status === 503 || // Service Unavailable status === 504 || // Gateway Timeout (status >= 500 && status < 600); // Any other server error } return false; } /** * Executes an operation with automatic retries on failure * Implements exponential backoff between retry attempts */ executeWithRetry(operation_1) { return __awaiter(this, arguments, void 0, function* (operation, attempt = 0) { try { return yield operation(); } catch (error) { if (attempt < this.retryConfig.maxRetries && this.shouldRetry(error)) { const backoffDelay = this.calculateBackoff(attempt); yield this.delay(backoffDelay); return this.executeWithRetry(operation, attempt + 1); } throw error; } }); } /** * Removes timestamps older than the configured time window * Helps maintain accurate count of recent requests */ cleanupOldTimestamps() { const now = Date.now(); this.requestTimestamps = this.requestTimestamps.filter(timestamp => now - timestamp < this.rateLimitConfig.timeWindowMs); } /** * Waits until a request slot becomes available * Ensures we don't exceed the configured rate limits */ waitForAvailableSlot() { return __awaiter(this, void 0, void 0, function* () { while (true) { this.cleanupOldTimestamps(); if (this.activeRequests < this.rateLimitConfig.maxConcurrent && this.requestTimestamps.length < this.rateLimitConfig.maxConcurrent) { return; } yield this.delay(100); // Check again after 100ms } }); } /** * Main method to execute an operation with rate limiting and retry logic * Ensures operations don't exceed rate limits and handles retries on failure */ execute(operation) { return __awaiter(this, void 0, void 0, function* () { yield this.waitForAvailableSlot(); this.activeRequests++; this.requestTimestamps.push(Date.now()); try { return yield this.executeWithRetry(operation); } finally { this.activeRequests--; } }); } } exports.RateLimiter = RateLimiter; //# sourceMappingURL=RateLimiter.js.map