UNPKG

@sudowealth/schwab-api

Version:

TypeScript client for Charles Schwab API with OAuth support, market data, trading functionality, and complete type safety

122 lines (121 loc) 4.8 kB
import { getMetadata, cloneRequestWithMetadata } from './middleware-metadata'; /** * Default rate limit options */ const DEFAULT_RATE_LIMIT = { maxRequests: 120, windowMs: 60_000, advanced: { applyToRetries: true, }, }; /** * Creates middleware that implements client-side rate limiting * * This middleware prevents your application from exceeding API rate limits * by queueing requests that would exceed the limit, then processing them * when the rate limit window resets. * * @param options Rate limit configuration options * @returns Middleware function */ export function withRateLimit(options) { // Merge provided options with defaults const config = { ...DEFAULT_RATE_LIMIT, ...options, advanced: { ...DEFAULT_RATE_LIMIT.advanced, ...options?.advanced, }, }; const maxRequests = config.maxRequests; const windowDuration = config.windowMs; const applyToRetries = config.advanced?.applyToRetries; let requestCount = 0; let windowStart = Date.now(); const requestQueue = []; const processQueue = () => { if (requestQueue.length === 0) return; const now = Date.now(); if (now - windowStart > windowDuration) { requestCount = 0; windowStart = now; } if (requestCount < maxRequests) { requestCount++; const resolveRequest = requestQueue.shift(); if (resolveRequest) { resolveRequest(); } } else { // Calculate time until the window resets and the next request can be processed const delay = windowStart + windowDuration - now; setTimeout(processQueue, delay > 0 ? delay : 0); } }; return async (req, next) => { // Get middleware metadata const metadata = getMetadata(req); // Check if this is a retry attempt that should skip rate limiting // Skip if it's a retry attempt and either: // 1. The retry middleware explicitly requested to skip rate limiting, or // 2. We're configured to not apply rate limiting to retries if (metadata.retry?.isRetry && (metadata.retry.skipRateLimit === true || !applyToRetries)) { // Skip rate limiting for retry attempts return next(req); } return new Promise((resolve) => { const attemptRequest = async () => { const now = Date.now(); if (now - windowStart > windowDuration) { requestCount = 0; windowStart = now; } if (requestCount < maxRequests) { requestCount++; // Update the rate limit metadata metadata.rateLimit = { remaining: maxRequests - requestCount, resetAt: windowStart + windowDuration, limit: maxRequests, wasQueued: false, }; // Clone the request with updated metadata const requestWithMetadata = cloneRequestWithMetadata(req); // Execute the next middleware const response = await next(requestWithMetadata); // Add rate limit info to the response metadata const responseMetadata = getMetadata(response); responseMetadata.rateLimit = { ...metadata.rateLimit, remaining: maxRequests - requestCount, }; resolve(response); } else { // Queue the request if rate limit is exceeded metadata.rateLimit = { remaining: 0, resetAt: windowStart + windowDuration, limit: maxRequests, wasQueued: true, }; requestQueue.push(() => { // Re-check conditions when dequeued, as window might have reset void attemptRequest(); // Explicitly mark as ignored with void operator }); // Ensure the queue is processed if this is the first queued item if (requestQueue.length === 1) { const delay = windowStart + windowDuration - Date.now(); setTimeout(processQueue, delay > 0 ? delay : 0); } } }; void attemptRequest(); // Explicitly mark as ignored with void operator }); }; }