@fal-ai/client
Version:
The fal.ai client for JavaScript and TypeScript
153 lines • 6.01 kB
JavaScript
;
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