opensea-js
Version:
TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data
148 lines (127 loc) • 4.52 kB
text/typescript
import { OpenSeaRateLimitError } from "../types";
import { pluralize } from "./stringHelper";
/**
* Default configuration for rate limit handling
*/
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_BASE_RETRY_DELAY_MS = 1000;
const EXPONENTIAL_BACKOFF_BASE = 2;
const MILLISECONDS_PER_SECOND = 1000;
/**
* HTTP status codes that indicate rate limiting
*/
const RATE_LIMIT_STATUS_CODE = 429;
const CUSTOM_RATE_LIMIT_STATUS_CODE = 599;
/**
* Options for handling rate-limited operations with retries.
* This is exported for SDK consumers who may want to use executeWithRateLimit
* for their own OpenSea API integrations.
*/
// eslint-disable-next-line import/no-unused-modules
export interface RateLimitOptions {
/** Logger function for logging progress */
logger?: (message: string) => void;
/** Maximum number of retry attempts for rate limit errors */
maxRetries?: number;
/** Base delay in ms to wait after a rate limit error if retry-after header is not present */
baseRetryDelay?: number;
}
/**
* Sleep for a specified duration
* @param ms Duration in milliseconds
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Execute an async operation with automatic retry on rate limit errors.
* Respects the retry-after header when present, otherwise uses exponential backoff.
*
* @param operation The async operation to execute
* @param options Configuration for rate limit handling
* @returns The result of the operation
* @throws The last error encountered if all retries are exhausted
*/
export async function executeWithRateLimit<T>(
operation: () => Promise<T>,
options: RateLimitOptions = {},
): Promise<T> {
const {
logger = () => {},
maxRetries = DEFAULT_MAX_RETRIES,
baseRetryDelay = DEFAULT_BASE_RETRY_DELAY_MS,
} = options;
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
const rateLimitError = error as OpenSeaRateLimitError;
// Check if this is a rate limit error by status code (robust) or retry-after header
const isRateLimitError =
rateLimitError.statusCode === RATE_LIMIT_STATUS_CODE ||
rateLimitError.statusCode === CUSTOM_RATE_LIMIT_STATUS_CODE ||
rateLimitError.retryAfter !== undefined;
if (!isRateLimitError || attempt === maxRetries) {
// Not a rate limit error or out of retries, throw the error
throw error;
}
// Calculate delay
let delayMs: number;
if (rateLimitError.retryAfter !== undefined) {
delayMs = rateLimitError.retryAfter * MILLISECONDS_PER_SECOND;
logger(
`Rate limit hit. Waiting ${rateLimitError.retryAfter} seconds before retry (attempt ${attempt + 1}/${maxRetries})...`,
);
} else {
// Exponential backoff
delayMs = baseRetryDelay * Math.pow(EXPONENTIAL_BACKOFF_BASE, attempt);
logger(
`Rate limit hit. Waiting ${delayMs}ms before retry (attempt ${attempt + 1}/${maxRetries})...`,
);
}
await sleep(delayMs);
logger(`Retrying operation...`);
}
}
// Should never reach here, but TypeScript needs this
throw lastError;
}
/**
* Execute an array of async operations sequentially with rate limit handling.
* Logs progress after each operation.
*
* @param operations Array of async operations to execute
* @param options Configuration for rate limit handling and progress logging
* @returns Array of results from each operation
*/
export async function executeSequentialWithRateLimit<T>(
operations: Array<() => Promise<T>>,
options: RateLimitOptions & {
operationName?: string;
} = {},
): Promise<T[]> {
const {
logger = () => {},
operationName = "operation",
...rateLimitOptions
} = options;
const results: T[] = [];
const total = operations.length;
logger(`Starting ${total} ${pluralize(total, operationName)}...`);
for (let i = 0; i < operations.length; i++) {
const operation = operations[i];
logger(`Executing ${operationName} ${i + 1}/${total}...`);
const result = await executeWithRateLimit(operation, {
...rateLimitOptions,
logger,
});
results.push(result);
logger(`Completed ${operationName} ${i + 1}/${total}`);
}
logger(
`All ${total} ${pluralize(total, operationName)} completed successfully`,
);
return results;
}