UNPKG

@fal-ai/client

Version:

The fal.ai client for JavaScript and TypeScript

153 lines 6.01 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.DEFAULT_RETRY_OPTIONS = exports.DEFAULT_RETRYABLE_STATUS_CODES = void 0; exports.isRetryableNetworkError = isRetryableNetworkError; exports.isRetryableError = isRetryableError; exports.calculateBackoffDelay = calculateBackoffDelay; exports.executeWithRetry = executeWithRetry; const response_1 = require("./response"); const utils_1 = require("./utils"); /** * Base retryable status codes for most requests */ exports.DEFAULT_RETRYABLE_STATUS_CODES = [429, 502, 503, 504]; exports.DEFAULT_RETRY_OPTIONS = { maxRetries: 3, baseDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, retryableStatusCodes: exports.DEFAULT_RETRYABLE_STATUS_CODES, enableJitter: true, }; /** * Network/transport-level error codes that indicate transient failures worth * retrying. Node's `fetch` (undici) wraps the underlying SystemError inside a * `TypeError("fetch failed")` and exposes the original via `.cause`; other * fetch implementations such as `node-fetch` set `.code` directly on the error. */ const RETRYABLE_NETWORK_ERROR_CODES = new Set([ "ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ENOTFOUND", "EPIPE", "ETIMEDOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_SOCKET", ]); /** * Returns true for transient transport-level failures (connection resets, * DNS hiccups, socket timeouts, etc.). Mirrors the Python client's behavior * of retrying `httpx.TransportError` and `httpx.TimeoutException`. */ function isRetryableNetworkError(error) { if (!error || typeof error !== "object") { return false; } // Walk the cause chain. User cancellation (AbortError) and signal-driven // timeouts (TimeoutError, e.g. AbortSignal.timeout) are explicit user // intent — never retry them, even if buried in a wrapper. const seen = new Set(); let current = error; let sawTransportShape = false; while (current && typeof current === "object" && !seen.has(current)) { seen.add(current); const name = current.name; if (name === "AbortError" || name === "TimeoutError") { return false; } const code = current.code; if (typeof code === "string" && RETRYABLE_NETWORK_ERROR_CODES.has(code)) { sawTransportShape = true; } current = current.cause; } if (sawTransportShape) { return true; } // Generic Node `fetch` failure (`TypeError: fetch failed`) without a // recognised `.code` — still a transport-layer problem, treat it as // retryable like httpx.TransportError does. if (error instanceof TypeError && typeof error.message === "string" && /fetch failed/i.test(error.message)) { return true; } return false; } /** * Determines if an error is retryable based on the status code or, for * non-HTTP failures, on the underlying transport error. User-specified * timeouts (504 with X-Fal-Request-Timeout-Type: user) are NOT retryable. */ function isRetryableError(error, retryableStatusCodes) { if (error instanceof response_1.ApiError) { // User-specified timeouts should NOT be retried if (error.isUserTimeout) { return false; } return retryableStatusCodes.includes(error.status); } return isRetryableNetworkError(error); } /** * Calculates the backoff delay for a given attempt using exponential backoff */ function calculateBackoffDelay(attempt, baseDelay, maxDelay, backoffMultiplier, enableJitter) { const exponentialDelay = Math.min(baseDelay * Math.pow(backoffMultiplier, attempt), maxDelay); if (enableJitter) { // Add ±25% jitter to prevent thundering herd const jitter = 0.25 * exponentialDelay * (Math.random() * 2 - 1); return Math.max(0, exponentialDelay + jitter); } return exponentialDelay; } /** * Executes an operation with retry logic and returns both result and metrics */ function executeWithRetry(operation, options, onRetry) { return __awaiter(this, void 0, void 0, function* () { const metrics = { totalAttempts: 0, totalDelay: 0, }; let lastError; for (let attempt = 0; attempt <= options.maxRetries; attempt++) { metrics.totalAttempts++; try { const result = yield operation(); return { result, metrics }; } catch (error) { lastError = error; metrics.lastError = error; if (attempt === options.maxRetries || !isRetryableError(error, options.retryableStatusCodes)) { throw error; } const delay = calculateBackoffDelay(attempt, options.baseDelay, options.maxDelay, options.backoffMultiplier, options.enableJitter); metrics.totalDelay += delay; if (onRetry) { onRetry(attempt + 1, error, delay); } yield (0, utils_1.sleep)(delay); } } throw lastError; }); } //# sourceMappingURL=retry.js.map