@noves/noves-sdk
Version:
Noves Developer Kit
1,311 lines (1,305 loc) • 213 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// node_modules/tsup/assets/esm_shims.js
import { fileURLToPath } from "url";
import path from "path";
var init_esm_shims = __esm({
"node_modules/tsup/assets/esm_shims.js"() {
"use strict";
}
});
// src/errors/ErrorTypes.ts
var ErrorType, ERROR_MESSAGES, ERROR_STATUS_CODES;
var init_ErrorTypes = __esm({
"src/errors/ErrorTypes.ts"() {
"use strict";
init_esm_shims();
ErrorType = /* @__PURE__ */ ((ErrorType2) => {
ErrorType2["UNAUTHORIZED"] = "UNAUTHORIZED";
ErrorType2["INVALID_API_KEY"] = "INVALID_API_KEY";
ErrorType2["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
ErrorType2["JOB_NOT_FOUND"] = "JOB_NOT_FOUND";
ErrorType2["JOB_PROCESSING"] = "JOB_PROCESSING";
ErrorType2["JOB_NOT_READY"] = "JOB_NOT_READY";
ErrorType2["INVALID_REQUEST"] = "INVALID_REQUEST";
ErrorType2["INVALID_RESPONSE_FORMAT"] = "INVALID_RESPONSE_FORMAT";
ErrorType2["NETWORK_ERROR"] = "NETWORK_ERROR";
ErrorType2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
ErrorType2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
return ErrorType2;
})(ErrorType || {});
ERROR_MESSAGES = {
["UNAUTHORIZED" /* UNAUTHORIZED */]: "Unauthorized",
["INVALID_API_KEY" /* INVALID_API_KEY */]: "Invalid API Key",
["RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */]: "Rate limit exceeded",
["JOB_NOT_FOUND" /* JOB_NOT_FOUND */]: "Job does not exist",
["JOB_PROCESSING" /* JOB_PROCESSING */]: "Job is still processing. Please try again in a few moments.",
["JOB_NOT_READY" /* JOB_NOT_READY */]: "Job not ready yet",
["INVALID_REQUEST" /* INVALID_REQUEST */]: "Invalid request",
["INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */]: "Invalid response format from API",
["NETWORK_ERROR" /* NETWORK_ERROR */]: "Network error occurred",
["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: "An unknown error occurred",
["VALIDATION_ERROR" /* VALIDATION_ERROR */]: "Validation error occurred"
};
ERROR_STATUS_CODES = {
["UNAUTHORIZED" /* UNAUTHORIZED */]: 401,
["INVALID_API_KEY" /* INVALID_API_KEY */]: 401,
["RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */]: 429,
["JOB_NOT_FOUND" /* JOB_NOT_FOUND */]: 404,
["JOB_PROCESSING" /* JOB_PROCESSING */]: 425,
["JOB_NOT_READY" /* JOB_NOT_READY */]: 425,
["INVALID_REQUEST" /* INVALID_REQUEST */]: 400,
["INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */]: 500,
["NETWORK_ERROR" /* NETWORK_ERROR */]: 500,
["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: 500,
["VALIDATION_ERROR" /* VALIDATION_ERROR */]: 400
};
}
});
// src/errors/TransactionError.ts
var TransactionError;
var init_TransactionError = __esm({
"src/errors/TransactionError.ts"() {
"use strict";
init_esm_shims();
init_ErrorTypes();
TransactionError = class extends Error {
errors;
errorType;
httpStatusCode;
details;
retryMetadata;
/**
* Creates an instance of TransactionError.
* @param {Record<string, string[]>} errors - The validation errors.
* @param {ErrorType} [errorType=ErrorType.UNKNOWN_ERROR] - The type of error.
* @param {number} [httpStatusCode] - The HTTP status code associated with the error.
* @param {any} [details] - Additional error details.
*/
constructor(errors, errorType = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */, httpStatusCode, details) {
const customMessage = errorType === "VALIDATION_ERROR" /* VALIDATION_ERROR */ && errors.message && errors.message.length > 0 ? errors.message[0] : void 0;
super(customMessage || ERROR_MESSAGES[errorType] || "Transaction validation error");
this.name = "TransactionError";
this.errors = errors;
this.errorType = errorType;
this.httpStatusCode = httpStatusCode;
this.details = details;
}
/**
* Check if this error is of a specific type.
* @param {ErrorType} type - The error type to check.
* @returns {boolean} True if the error matches the specified type.
*/
isErrorType(type) {
return this.errorType === type;
}
/**
* Check if this error indicates a job not found.
* @returns {boolean} True if the error indicates a job not found.
*/
isJobNotFound() {
return this.errorType === "JOB_NOT_FOUND" /* JOB_NOT_FOUND */;
}
/**
* Check if this error indicates a job is still processing.
* @returns {boolean} True if the error indicates a job is processing.
*/
isJobProcessing() {
return this.errorType === "JOB_PROCESSING" /* JOB_PROCESSING */ || this.errorType === "JOB_NOT_READY" /* JOB_NOT_READY */;
}
/**
* Check if this error indicates rate limiting.
* @returns {boolean} True if the error indicates rate limiting.
*/
isRateLimited() {
return this.errorType === "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */;
}
/**
* Check if this error indicates authorization issues.
* @returns {boolean} True if the error indicates authorization issues.
*/
isUnauthorized() {
return this.errorType === "UNAUTHORIZED" /* UNAUTHORIZED */ || this.errorType === "INVALID_API_KEY" /* INVALID_API_KEY */;
}
/**
* Check if this error has retry metadata attached.
* @returns {boolean} True if retry metadata is available.
*/
hasRetryMetadata() {
return this.retryMetadata !== void 0;
}
/**
* Get the number of retry attempts made before this final failure.
* @returns {number} Number of attempts, or 0 if no retry metadata.
*/
getRetryAttempts() {
return this.retryMetadata?.attempts || 0;
}
/**
* Get the total duration of all retry attempts.
* @returns {number} Total duration in milliseconds, or 0 if no retry metadata.
*/
getRetryDuration() {
return this.retryMetadata?.totalDuration || 0;
}
/**
* Get the recent performance assessment at time of failure.
* @returns {string} Performance level or 'unknown' if no retry metadata.
*/
getPerformanceLevel() {
return this.retryMetadata?.recentPerformance || "unknown";
}
/**
* Get a summary of the error pattern that led to this failure.
* @returns {string[]} Array of error types in chronological order.
*/
getErrorPattern() {
return this.retryMetadata?.errorHistory.map((e) => e.errorType) || [];
}
/**
* Check if this error resulted from exhausted retries.
* @returns {boolean} True if this error is the result of failed retry attempts.
*/
isRetryExhausted() {
return this.hasRetryMetadata() && this.getRetryAttempts() > 1;
}
/**
* Get a human-readable summary of retry information.
* @returns {string} Formatted summary of retry attempts and performance.
*/
getRetrySummary() {
if (!this.hasRetryMetadata()) {
return "No retry attempts made";
}
const attempts = this.getRetryAttempts();
const duration = this.getRetryDuration();
const performance = this.getPerformanceLevel();
const pattern = this.getErrorPattern();
return `${attempts} retry attempts over ${duration}ms (performance: ${performance}, pattern: ${pattern.join(" \u2192 ")})`;
}
};
}
});
// src/utils/apiUtils.ts
function mergeRetryConfig(baseConfig, userConfig) {
if (arguments.length === 1 && baseConfig) {
userConfig = baseConfig;
baseConfig = DEFAULT_RETRY_CONFIG;
}
const base = baseConfig || DEFAULT_RETRY_CONFIG;
if (!userConfig) {
return { ...base };
}
return {
...base,
...userConfig,
// Ensure arrays are properly merged, not replaced
retryableErrors: userConfig.retryableErrors || base.retryableErrors,
retryableStatusCodes: userConfig.retryableStatusCodes || base.retryableStatusCodes
};
}
function extractRetryAfterFromError(error) {
if (!error) return void 0;
if (error instanceof TransactionError && error.details?.retryAfter) {
return error.details.retryAfter;
}
if (error.response?.headers?.["retry-after"]) {
return parseInt(error.response.headers["retry-after"], 10);
}
return void 0;
}
function combineAbortSignals(...signals) {
if (signals.length === 1) return signals[0];
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
controller.abort(signal.reason);
return controller.signal;
}
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
}
return controller.signal;
}
function buildRequestSignal(fetchConfig, perRequestSignal) {
const signals = [];
let timeoutId;
let timeoutController;
if (fetchConfig?.timeout) {
timeoutController = new AbortController();
timeoutId = setTimeout(() => timeoutController.abort(), fetchConfig.timeout);
signals.push(timeoutController.signal);
}
if (fetchConfig?.signal) {
signals.push(fetchConfig.signal);
}
if (perRequestSignal) {
signals.push(perRequestSignal);
}
const cleanup = () => {
if (timeoutId) clearTimeout(timeoutId);
};
if (signals.length === 0) {
return { signal: void 0, cleanup };
}
if (signals.length === 1) {
return { signal: signals[0], cleanup };
}
return { signal: combineAbortSignals(...signals), cleanup };
}
async function processResponse(response) {
if (response.status === 429) {
const retryAfter = response.headers.get("retry-after");
const error = new Error("Rate limit exceeded");
error.response = { status: 429, headers: { "retry-after": retryAfter } };
throw error;
}
if (response.status === 401) {
return {
succeeded: false,
response: { message: "Unauthorized" },
httpStatusCode: 401,
errorType: "UNAUTHORIZED"
};
}
if (response.status === 425) {
const responseData2 = await response.json();
return {
succeeded: false,
response: { message: "Job not ready yet", detail: responseData2.detail },
httpStatusCode: 425,
errorType: "JOB_NOT_READY"
};
}
let responseData;
try {
responseData = await response.json();
} catch (error) {
return {
succeeded: false,
response: { message: "Invalid response format" },
httpStatusCode: response.status,
errorType: "INVALID_RESPONSE_FORMAT"
};
}
if (!response.ok) {
if ([425, 429, 500, 502, 503, 504].includes(response.status)) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.response = { status: response.status, data: responseData };
throw error;
}
return {
succeeded: false,
response: responseData,
httpStatusCode: response.status
};
}
return {
succeeded: true,
response: responseData,
httpStatusCode: response.status
};
}
function createTranslateClient(ecosystem, apiKey, retryConfig, fetchConfig) {
const config = mergeRetryConfig(retryConfig);
return async function request(endpoint, method = "GET", options = {}) {
const operationId = `translate-${ecosystem}-${endpoint}`;
try {
return await retryEngine.executeWithRetry(
async () => {
if (fetchConfig?.signal?.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
const url = `${TRANSLATE_URL}/${ecosystem}/${endpoint}`;
const { signal, cleanup } = buildRequestSignal(fetchConfig, options.signal);
try {
const response = await fetch(url, {
...options,
method,
signal,
headers: {
...fetchConfig?.headers,
...options.headers,
"apiKey": apiKey,
"Content-Type": "application/json"
}
});
return processResponse(response);
} catch (error) {
if (error.name === "AbortError") {
if (fetchConfig?.signal?.aborted) {
throw error;
}
if (fetchConfig?.timeout) {
const timeoutError = new Error(`Request timed out after ${fetchConfig.timeout}ms`);
timeoutError.code = "NETWORK_ERROR";
throw timeoutError;
}
throw error;
}
throw error;
} finally {
cleanup();
}
},
config,
operationId
);
} catch (error) {
if (error.name === "AbortError") {
throw error;
}
return {
succeeded: false,
response: { message: error.message || "Request failed after retries" },
httpStatusCode: error.response?.status || 500
};
}
};
}
function createForesightClient(apiKey, retryConfig, fetchConfig) {
const config = mergeRetryConfig(retryConfig);
return async function request(endpoint, method = "GET", options = {}) {
const operationId = `foresight-${endpoint}`;
try {
return await retryEngine.executeWithRetry(
async () => {
if (fetchConfig?.signal?.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
const { signal, cleanup } = buildRequestSignal(fetchConfig, options.signal);
try {
const response = await fetch(`${FORESIGHT_URL}/${endpoint}`, {
...options,
method,
signal,
headers: {
...fetchConfig?.headers,
...options.headers,
"apiKey": apiKey,
"Content-Type": "application/json"
}
});
return processResponse(response);
} catch (error) {
if (error.name === "AbortError") {
if (fetchConfig?.signal?.aborted) {
throw error;
}
if (fetchConfig?.timeout) {
const timeoutError = new Error(`Request timed out after ${fetchConfig.timeout}ms`);
timeoutError.code = "NETWORK_ERROR";
throw timeoutError;
}
throw error;
}
throw error;
} finally {
cleanup();
}
},
config,
operationId
);
} catch (error) {
if (error.name === "AbortError") {
throw error;
}
return {
succeeded: false,
response: { message: error.message || "Request failed after retries" },
httpStatusCode: error.response?.status || 500
};
}
};
}
function createPricingClient(ecosystem, apiKey, retryConfig, fetchConfig) {
const config = mergeRetryConfig(retryConfig);
return async function request(endpoint, method = "GET", options = {}) {
const operationId = `pricing-${ecosystem}-${endpoint}`;
try {
return await retryEngine.executeWithRetry(
async () => {
if (fetchConfig?.signal?.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
const { signal, cleanup } = buildRequestSignal(fetchConfig, options.signal);
try {
const response = await fetch(`${PRICING_URL}/${ecosystem}/${endpoint}`, {
...options,
method,
signal,
headers: {
...fetchConfig?.headers,
...options.headers,
"apiKey": apiKey,
"Content-Type": "application/json"
}
});
return processResponse(response);
} catch (error) {
if (error.name === "AbortError") {
if (fetchConfig?.signal?.aborted) {
throw error;
}
if (fetchConfig?.timeout) {
const timeoutError = new Error(`Request timed out after ${fetchConfig.timeout}ms`);
timeoutError.code = "NETWORK_ERROR";
throw timeoutError;
}
throw error;
}
throw error;
} finally {
cleanup();
}
},
config,
operationId
);
} catch (error) {
if (error.name === "AbortError") {
throw error;
}
return {
succeeded: false,
response: { message: error.message || "Request failed after retries" },
httpStatusCode: error.response?.status || 500
};
}
};
}
var TRANSLATE_URL, FORESIGHT_URL, PRICING_URL, DEFAULT_RETRY_CONFIG, RETRY_PRESETS, IntelligentRetryEngine, ERROR_SPECIFIC_STRATEGIES, RetryBudgetManager, PredictiveAnalyzer, IntelligentCircuitBreaker, retryBudgetManager, predictiveAnalyzer, intelligentCircuitBreaker, retryEngine;
var init_apiUtils = __esm({
"src/utils/apiUtils.ts"() {
"use strict";
init_esm_shims();
init_ErrorTypes();
init_TransactionError();
TRANSLATE_URL = "https://translate.noves.fi";
FORESIGHT_URL = "https://foresight.noves.fi";
PRICING_URL = "https://pricing.noves.fi";
DEFAULT_RETRY_CONFIG = {
enabled: false,
maxAttempts: 3,
baseDelay: 1e3,
maxDelay: 3e4,
exponentialBase: 2,
jitter: true,
jitterFactor: 0.1,
retryableErrors: [
"RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */,
"NETWORK_ERROR" /* NETWORK_ERROR */,
"JOB_NOT_READY" /* JOB_NOT_READY */,
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */
],
retryableStatusCodes: [429, 500, 502, 503, 504],
respectRetryAfter: true,
circuitBreakerThreshold: 5,
circuitBreakerResetTime: 6e4
};
RETRY_PRESETS = {
/**
* Completely disables retry mechanism (default for backward compatibility)
*/
disabled: {
...DEFAULT_RETRY_CONFIG,
enabled: false
},
/**
* Conservative retry strategy with minimal retries and longer delays.
* Suitable for rate-limited environments or when being respectful of server resources.
*/
conservative: {
...DEFAULT_RETRY_CONFIG,
enabled: true,
maxAttempts: 2,
baseDelay: 2e3,
maxDelay: 15e3,
circuitBreakerThreshold: 3
},
/**
* Standard retry strategy with balanced settings.
* Good default for most production applications.
*/
standard: {
...DEFAULT_RETRY_CONFIG,
enabled: true
},
/**
* Aggressive retry strategy with more attempts and shorter delays.
* Use when high availability is critical and server resources allow.
*/
aggressive: {
...DEFAULT_RETRY_CONFIG,
enabled: true,
maxAttempts: 5,
baseDelay: 500,
maxDelay: 2e4,
circuitBreakerThreshold: 8
},
/**
* Production-optimized retry strategy with circuit breaker tuned for high-traffic scenarios.
* Balances resilience with resource protection.
*/
production: {
...DEFAULT_RETRY_CONFIG,
enabled: true,
maxAttempts: 3,
baseDelay: 1e3,
maxDelay: 1e4,
circuitBreakerThreshold: 3,
circuitBreakerResetTime: 3e4
},
/**
* Fast-fail strategy with minimal retries for time-sensitive operations.
* Only retries obvious transient errors like rate limits.
*/
fastFail: {
...DEFAULT_RETRY_CONFIG,
enabled: true,
maxAttempts: 2,
baseDelay: 500,
maxDelay: 2e3,
retryableErrors: ["RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */, "NETWORK_ERROR" /* NETWORK_ERROR */],
circuitBreakerThreshold: 2,
circuitBreakerResetTime: 1e4
}
};
IntelligentRetryEngine = class {
adaptiveContexts = /* @__PURE__ */ new Map();
globalPerformanceMetrics = {
avgResponseTime: 1e3,
successRate: 0.95,
errorRateByType: /* @__PURE__ */ new Map()
};
/**
* Execute a request function with intelligent retry logic applied.
* @param requestFn Function that makes the actual request
* @param config Retry configuration to use
* @param operationId Unique identifier for this operation type
* @returns Promise that resolves with the request result
*/
async executeWithRetry(requestFn, config, operationId) {
if (!config.enabled) {
return requestFn();
}
if (!retryBudgetManager.canRetry(operationId)) {
throw new Error(`Retry budget exhausted for operation: ${operationId}`);
}
const context = this.getOrCreateAdaptiveContext(operationId);
const strategy = this.determineRetryStrategy(context, config);
if (this.isCircuitOpen(context, strategy, config)) {
throw new Error(`Circuit breaker is open for operation: ${operationId}`);
}
if (predictiveAnalyzer.shouldPreemptivelyFail(operationId, context)) {
throw new Error(`Predictive analysis suggests operation will fail: ${operationId}`);
}
let lastError;
const startTime = Date.now();
for (let attempt = 1; attempt <= strategy.maxAttempts; attempt++) {
try {
retryBudgetManager.consumeRetry(operationId);
const result = await requestFn();
const responseTime = Date.now() - startTime;
this.recordSuccess(context, responseTime);
this.updatePerformanceMetrics(operationId, true, responseTime);
predictiveAnalyzer.recordMetrics(operationId, true, responseTime);
if (config.onSuccess) {
config.onSuccess(attempt);
}
return result;
} catch (error) {
lastError = error;
const errorType = this.extractErrorType(error);
this.recordError(context, errorType);
predictiveAnalyzer.recordMetrics(operationId, false, Date.now() - startTime, errorType);
if (attempt === strategy.maxAttempts || !strategy.shouldRetry(attempt, error, context)) {
this.recordFailure(context);
this.updatePerformanceMetrics(operationId, false, Date.now() - startTime);
if (config.onFailure) {
config.onFailure(attempt, error);
}
throw this.enhanceErrorWithRetryInfo(error, attempt, context);
}
const delay = strategy.calculateDelay(attempt, strategy.baseDelay, context);
if (config.onRetry) {
config.onRetry(attempt, error, delay);
}
await this.delay(delay);
}
}
throw lastError;
}
/**
* Determine the optimal retry strategy based on error type and context.
*/
determineRetryStrategy(context, config) {
if (context.errorHistory.length > 0) {
const lastErrorType = context.errorHistory[context.errorHistory.length - 1].errorType;
if (ERROR_SPECIFIC_STRATEGIES[lastErrorType]) {
return this.adaptStrategy(ERROR_SPECIFIC_STRATEGIES[lastErrorType], context, config);
}
}
return this.createGeneralStrategy(context, config);
}
/**
* Adapt strategy based on recent performance and context.
*/
adaptStrategy(baseStrategy, context, config) {
const adapted = { ...baseStrategy };
if (context.recentPerformance === "poor") {
adapted.maxAttempts = Math.max(1, Math.floor(adapted.maxAttempts * 0.7));
adapted.baseDelay = Math.floor(adapted.baseDelay * 1.5);
} else if (context.recentPerformance === "good" && context.successfulRecoveries > 3) {
adapted.maxAttempts = Math.min(10, Math.floor(adapted.maxAttempts * 1.2));
adapted.baseDelay = Math.floor(adapted.baseDelay * 0.8);
}
if (config.maxAttempts !== DEFAULT_RETRY_CONFIG.maxAttempts) {
adapted.maxAttempts = config.maxAttempts;
}
if (config.baseDelay !== DEFAULT_RETRY_CONFIG.baseDelay) {
adapted.baseDelay = config.baseDelay;
}
return adapted;
}
/**
* Create a general strategy when no error-specific strategy is available.
*/
createGeneralStrategy(context, config) {
return {
errorType: "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
maxAttempts: config.maxAttempts,
baseDelay: config.baseDelay,
maxDelay: config.maxDelay,
backoffType: "exponential",
jitterFactor: config.jitterFactor,
shouldRetry: (attempt) => attempt <= config.maxAttempts,
calculateDelay: (attempt, baseDelay) => {
return Math.min(baseDelay * Math.pow(config.exponentialBase, attempt - 1), config.maxDelay);
}
};
}
/**
* Extract error type from various error formats.
*/
extractErrorType(error) {
if (error instanceof TransactionError) {
return error.errorType;
}
if (error.response?.status === 429) {
return "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */;
}
if (error.response?.status === 401) {
return "UNAUTHORIZED" /* UNAUTHORIZED */;
}
if (error.response?.status === 425) {
return "JOB_NOT_READY" /* JOB_NOT_READY */;
}
if (error instanceof TypeError || error.code === "NETWORK_ERROR") {
return "NETWORK_ERROR" /* NETWORK_ERROR */;
}
return "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
}
/**
* Record error occurrence and update context.
*/
recordError(context, errorType) {
context.errorHistory.push({
errorType,
timestamp: Date.now()
});
if (context.errorHistory.length > 50) {
context.errorHistory = context.errorHistory.slice(-50);
}
context.lastError = errorType;
this.updatePerformanceAssessment(context);
}
/**
* Record successful operation and recovery metrics.
*/
recordSuccess(context, responseTime) {
if (context.errorHistory.length > 0) {
const lastError = context.errorHistory[context.errorHistory.length - 1];
if (!lastError.recoveryTime) {
lastError.recoveryTime = Date.now() - lastError.timestamp;
context.successfulRecoveries++;
const recoveryTimes = context.errorHistory.filter((e) => e.recoveryTime).map((e) => e.recoveryTime);
if (recoveryTimes.length > 0) {
context.avgRecoveryTime = recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length;
}
}
}
this.updatePerformanceAssessment(context);
}
/**
* Update performance assessment based on recent patterns.
*/
updatePerformanceAssessment(context) {
const recentErrors = context.errorHistory.filter(
(e) => Date.now() - e.timestamp < 3e5
// Last 5 minutes
);
const errorRate = recentErrors.length / Math.max(context.successfulRecoveries + recentErrors.length, 1);
if (errorRate > 0.3) {
context.recentPerformance = "poor";
} else if (errorRate > 0.1) {
context.recentPerformance = "degraded";
} else {
context.recentPerformance = "good";
}
}
/**
* Check if circuit breaker should prevent requests.
*/
isCircuitOpen(context, strategy, config) {
if (strategy.errorType) {
return !intelligentCircuitBreaker.shouldAllowRequest(
context.operationId,
strategy.errorType,
config
);
}
if (context.circuitBreakerState === "OPEN") {
const now = Date.now();
if (now - context.lastFailureTime > config.circuitBreakerResetTime) {
context.circuitBreakerState = "HALF_OPEN";
return false;
}
return true;
}
return false;
}
/**
* Record failure and update circuit breaker state.
*/
recordFailure(context) {
context.failures++;
context.lastFailureTime = Date.now();
if (context.failures >= 5) {
context.circuitBreakerState = "OPEN";
}
}
/**
* Enhance error with retry metadata for debugging.
*/
enhanceErrorWithRetryInfo(error, attempts, context) {
if (error instanceof TransactionError) {
error.retryMetadata = {
attempts,
totalDuration: Date.now() - context.startTime,
errorHistory: context.errorHistory.slice(-5),
// Last 5 errors
recentPerformance: context.recentPerformance
};
}
return error;
}
/**
* Get or create adaptive context for operation tracking.
*/
getOrCreateAdaptiveContext(operationId) {
if (!this.adaptiveContexts.has(operationId)) {
this.adaptiveContexts.set(operationId, {
operationId,
startTime: Date.now(),
failures: 0,
lastFailureTime: 0,
circuitBreakerState: "CLOSED",
errorHistory: [],
successfulRecoveries: 0,
avgRecoveryTime: 0,
recentPerformance: "good"
});
}
return this.adaptiveContexts.get(operationId);
}
/**
* Update global performance metrics for adaptive decisions.
*/
updatePerformanceMetrics(operationId, success, responseTime) {
if (success) {
this.globalPerformanceMetrics.avgResponseTime = this.globalPerformanceMetrics.avgResponseTime * 0.9 + responseTime * 0.1;
}
this.globalPerformanceMetrics.successRate = this.globalPerformanceMetrics.successRate * 0.95 + (success ? 0.05 : 0);
}
/**
* Delay execution for the specified number of milliseconds.
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
ERROR_SPECIFIC_STRATEGIES = {
["RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */]: {
errorType: "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */,
maxAttempts: 5,
baseDelay: 2e3,
maxDelay: 6e4,
backoffType: "exponential",
jitterFactor: 0.25,
// Higher jitter to spread out requests
shouldRetry: (attempt, error, context) => {
return attempt <= 5;
},
calculateDelay: (attempt, baseDelay, context) => {
const retryAfter = extractRetryAfterFromError(context.lastError);
if (retryAfter) {
return Math.min(retryAfter * 1e3, 6e4);
}
return Math.min(baseDelay * Math.pow(2, attempt - 1), 6e4);
}
},
["JOB_NOT_READY" /* JOB_NOT_READY */]: {
errorType: "JOB_NOT_READY" /* JOB_NOT_READY */,
maxAttempts: 10,
baseDelay: 3e3,
maxDelay: 3e4,
backoffType: "linear",
// Jobs typically become ready at predictable intervals
jitterFactor: 0.1,
shouldRetry: (attempt, error, context) => {
const timeSinceStart = Date.now() - context.startTime;
return attempt <= 10 && timeSinceStart < 3e5;
},
calculateDelay: (attempt, baseDelay, context) => {
return Math.min(baseDelay + attempt * 1e3, 3e4);
}
},
["JOB_PROCESSING" /* JOB_PROCESSING */]: {
errorType: "JOB_PROCESSING" /* JOB_PROCESSING */,
maxAttempts: 8,
baseDelay: 5e3,
maxDelay: 45e3,
backoffType: "linear",
jitterFactor: 0.15,
shouldRetry: (attempt, error, context) => {
const timeSinceStart = Date.now() - context.startTime;
return attempt <= 8 && timeSinceStart < 42e4;
},
calculateDelay: (attempt, baseDelay, context) => {
return Math.min(baseDelay + attempt * 2e3, 45e3);
}
},
["NETWORK_ERROR" /* NETWORK_ERROR */]: {
errorType: "NETWORK_ERROR" /* NETWORK_ERROR */,
maxAttempts: 4,
baseDelay: 1e3,
maxDelay: 15e3,
backoffType: "exponential",
jitterFactor: 0.2,
shouldRetry: (attempt, error, context) => {
return attempt <= 4;
},
calculateDelay: (attempt, baseDelay, context) => {
return Math.min(baseDelay * Math.pow(2, attempt - 1), 15e3);
}
},
["INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */]: {
errorType: "INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */,
maxAttempts: 3,
baseDelay: 1500,
maxDelay: 1e4,
backoffType: "exponential",
jitterFactor: 0.1,
shouldRetry: (attempt, error, context) => {
return attempt <= 3;
},
calculateDelay: (attempt, baseDelay, context) => {
return Math.min(baseDelay * Math.pow(1.5, attempt - 1), 1e4);
}
},
["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: {
errorType: "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
maxAttempts: 2,
baseDelay: 2e3,
maxDelay: 8e3,
backoffType: "exponential",
jitterFactor: 0.15,
shouldRetry: (attempt, error, context) => {
return attempt <= 2;
},
calculateDelay: (attempt, baseDelay, context) => {
return Math.min(baseDelay * Math.pow(2, attempt - 1), 8e3);
}
},
// Non-retryable errors
["UNAUTHORIZED" /* UNAUTHORIZED */]: {
errorType: "UNAUTHORIZED" /* UNAUTHORIZED */,
maxAttempts: 0,
// Never retry auth errors
baseDelay: 0,
maxDelay: 0,
backoffType: "fixed",
jitterFactor: 0,
shouldRetry: () => false,
calculateDelay: () => 0
},
["INVALID_API_KEY" /* INVALID_API_KEY */]: {
errorType: "INVALID_API_KEY" /* INVALID_API_KEY */,
maxAttempts: 0,
baseDelay: 0,
maxDelay: 0,
backoffType: "fixed",
jitterFactor: 0,
shouldRetry: () => false,
calculateDelay: () => 0
},
["VALIDATION_ERROR" /* VALIDATION_ERROR */]: {
errorType: "VALIDATION_ERROR" /* VALIDATION_ERROR */,
maxAttempts: 0,
baseDelay: 0,
maxDelay: 0,
backoffType: "fixed",
jitterFactor: 0,
shouldRetry: () => false,
calculateDelay: () => 0
},
["INVALID_REQUEST" /* INVALID_REQUEST */]: {
errorType: "INVALID_REQUEST" /* INVALID_REQUEST */,
maxAttempts: 0,
baseDelay: 0,
maxDelay: 0,
backoffType: "fixed",
jitterFactor: 0,
shouldRetry: () => false,
calculateDelay: () => 0
},
["JOB_NOT_FOUND" /* JOB_NOT_FOUND */]: {
errorType: "JOB_NOT_FOUND" /* JOB_NOT_FOUND */,
maxAttempts: 0,
baseDelay: 0,
maxDelay: 0,
backoffType: "fixed",
jitterFactor: 0,
shouldRetry: () => false,
calculateDelay: () => 0
}
};
RetryBudgetManager = class {
budgets = /* @__PURE__ */ new Map();
canRetry(operationId, maxRetriesPerWindow = 50, windowSizeMs = 6e4) {
const budget = this.getOrCreateBudget(operationId, maxRetriesPerWindow, windowSizeMs);
if (Date.now() - budget.windowStart > budget.windowSizeMs) {
budget.currentRetries = 0;
budget.windowStart = Date.now();
}
return budget.currentRetries < budget.maxRetries;
}
consumeRetry(operationId) {
const budget = this.budgets.get(operationId);
if (budget) {
budget.currentRetries++;
}
}
getOrCreateBudget(operationId, maxRetries, windowSizeMs) {
if (!this.budgets.has(operationId)) {
this.budgets.set(operationId, {
maxRetries,
windowSizeMs,
currentRetries: 0,
windowStart: Date.now()
});
}
return this.budgets.get(operationId);
}
};
PredictiveAnalyzer = class {
operationMetrics = /* @__PURE__ */ new Map();
shouldPreemptivelyFail(operationId, context) {
const metrics = this.operationMetrics.get(operationId);
if (!metrics) return false;
const recentErrors = metrics.errorPatterns.filter(
(e) => Date.now() - e.timestamp < 3e4
// Last 30 seconds
);
if (recentErrors.length > 10) {
return true;
}
const lastFiveErrors = context.errorHistory.slice(-5);
if (lastFiveErrors.length === 5) {
const errorTypes = lastFiveErrors.map((e) => e.errorType);
const uniqueTypes = new Set(errorTypes);
if (uniqueTypes.size === 1 && ["UNAUTHORIZED" /* UNAUTHORIZED */, "INVALID_API_KEY" /* INVALID_API_KEY */, "VALIDATION_ERROR" /* VALIDATION_ERROR */].includes(lastFiveErrors[0].errorType)) {
return true;
}
}
return false;
}
recordMetrics(operationId, success, responseTime, errorType) {
if (!this.operationMetrics.has(operationId)) {
this.operationMetrics.set(operationId, {
recentLatencies: [],
errorPatterns: [],
successRate: 1,
avgResponseTime: responseTime
});
}
const metrics = this.operationMetrics.get(operationId);
if (success) {
metrics.recentLatencies.push(responseTime);
if (metrics.recentLatencies.length > 20) {
metrics.recentLatencies = metrics.recentLatencies.slice(-20);
}
metrics.avgResponseTime = metrics.avgResponseTime * 0.9 + responseTime * 0.1;
metrics.successRate = metrics.successRate * 0.95 + 0.05;
} else if (errorType) {
metrics.errorPatterns.push({ timestamp: Date.now(), errorType });
if (metrics.errorPatterns.length > 50) {
metrics.errorPatterns = metrics.errorPatterns.slice(-50);
}
metrics.successRate = metrics.successRate * 0.95;
}
}
getHealthScore(operationId) {
const metrics = this.operationMetrics.get(operationId);
if (!metrics) return 1;
const responseTimeHealth = metrics.avgResponseTime < 5e3 ? 1 : metrics.avgResponseTime < 15e3 ? 0.7 : 0.3;
return metrics.successRate * 0.7 + responseTimeHealth * 0.3;
}
};
IntelligentCircuitBreaker = class {
circuits = /* @__PURE__ */ new Map();
shouldAllowRequest(operationId, errorType, config) {
const circuitMap = this.getOrCreateCircuitMap(operationId);
const circuit = this.getOrCreateCircuit(circuitMap, errorType);
switch (circuit.state) {
case "CLOSED":
return true;
case "OPEN":
const timeSinceLastFailure = Date.now() - circuit.lastFailureTime;
const resetTime = this.getResetTimeForErrorType(errorType, config);
if (timeSinceLastFailure > resetTime) {
circuit.state = "HALF_OPEN";
return true;
}
return false;
case "HALF_OPEN":
return true;
}
}
recordSuccess(operationId, errorType) {
const circuitMap = this.circuits.get(operationId);
if (circuitMap) {
const circuit = circuitMap.get(errorType);
if (circuit) {
circuit.failures = 0;
circuit.state = "CLOSED";
}
}
}
recordFailure(operationId, errorType, config) {
const circuitMap = this.getOrCreateCircuitMap(operationId);
const circuit = this.getOrCreateCircuit(circuitMap, errorType);
circuit.failures++;
circuit.lastFailureTime = Date.now();
const threshold = this.getThresholdForErrorType(errorType, config);
if (circuit.failures >= threshold) {
circuit.state = "OPEN";
}
}
getThresholdForErrorType(errorType, config) {
switch (errorType) {
case "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */:
return 3;
case "NETWORK_ERROR" /* NETWORK_ERROR */:
return 5;
case "JOB_NOT_READY" /* JOB_NOT_READY */:
case "JOB_PROCESSING" /* JOB_PROCESSING */:
return 8;
default:
return config.circuitBreakerThreshold;
}
}
getResetTimeForErrorType(errorType, config) {
switch (errorType) {
case "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */:
return 6e4;
case "NETWORK_ERROR" /* NETWORK_ERROR */:
return 3e4;
case "JOB_NOT_READY" /* JOB_NOT_READY */:
case "JOB_PROCESSING" /* JOB_PROCESSING */:
return 12e4;
default:
return config.circuitBreakerResetTime;
}
}
getOrCreateCircuitMap(operationId) {
if (!this.circuits.has(operationId)) {
this.circuits.set(operationId, /* @__PURE__ */ new Map());
}
return this.circuits.get(operationId);
}
getOrCreateCircuit(circuitMap, errorType) {
if (!circuitMap.has(errorType)) {
circuitMap.set(errorType, {
failures: 0,
lastFailureTime: 0,
state: "CLOSED",
errorType
});
}
return circuitMap.get(errorType);
}
};
retryBudgetManager = new RetryBudgetManager();
predictiveAnalyzer = new PredictiveAnalyzer();
intelligentCircuitBreaker = new IntelligentCircuitBreaker();
retryEngine = new IntelligentRetryEngine();
}
});
// src/foresight/foresight.ts
var foresight_exports = {};
__export(foresight_exports, {
Foresight: () => Foresight
});
var Foresight;
var init_foresight = __esm({
"src/foresight/foresight.ts"() {
"use strict";
init_esm_shims();
init_TransactionError();
init_apiUtils();
Foresight = class {
request;
/**
* Create a Foresight instance.
* @param {string} apiKey - The API key to authenticate requests.
* @param {Partial<RetryConfig>} [retryConfig] - Optional retry configuration for enhanced resilience.
* @param {FetchConfig} [fetchConfig] - Optional fetch configuration for timeouts, abort signals, and custom headers.
* @throws Will throw an error if the API key is not provided.
*/
constructor(apiKey, retryConfig, fetchConfig) {
if (!apiKey) {
throw new Error("API key is required");
}
this.request = createForesightClient(apiKey, retryConfig, fetchConfig);
}
/**
* Returns a list with the names of the EVM blockchains currently supported by this API.
* Use the provided chain name when calling other methods.
* @returns {Promise<EVMForesightChains>} A promise that resolves to an array of chains.
*/
async getChains() {
const result = await this.request("evm/chains");
return result.response;
}
/**
* Takes an unsigned transaction object and returns a fully classified transaction,
* including an enriched English description of the action that is about to take place, and all relevant asset transfers tagged.
*
* Optionally, it takes a stateOverrides object, which allows you to customize the state of the chain before the transaction is previewed.
* Useful for more advanced applications. You can skip this object to preview the transaction in the "real" state of the chain.
* @param {string} chain - The chain name.
* @param {EVMTranslateUnsignedTransaction} unsignedTransaction - The unsigned transaction object, modeled after the standard format used by multiple EVM wallets.
* @param {EVMTranslateStateOverrides} stateOverrides - OPTIONAL. The state overrides object allows you to customize the state of the chain before the transaction is previewed.
* @param {string} viewAsAccountAddress - OPTIONAL The account address from which perspective the transaction will be previewed. Leave blank to use the raw 'from' of the transaction object.
* @param {number} block - OPTIONAL. The block number to preview the transaction at. Leave blank to use the latest block number.
* @returns {Promise<EVMForesightPreviewResponse>} A promise that resolves to the transaction preview details.
* @throws {TransactionError} If there are validation errors in the request.
*/
async preview(chain, unsignedTransaction, stateOverrides, viewAsAccountAddress, block) {
try {
let endpoint = `evm/${chain}/preview`;
const queryParams = new URLSearchParams();
if (block) {
queryParams.append("block", block.toString());
}
if (viewAsAccountAddress) {
queryParams.append("viewAsAccountAddress", viewAsAccountAddress);
}
if (queryParams.toString()) {
endpoint += `?${queryParams.toString()}`;
}
const body = {
transaction: unsignedTransaction,
stateOverrides: stateOverrides || {}
};
const result = await this.request(endpoint, "POST", { body: JSON.stringify(body) });
return result.response;
} catch (error) {
if (error instanceof Response) {
const errorResponse = await error.json();
if (errorResponse.status === 400 && errorResponse.errors) {
throw new TransactionError(errorResponse.errors);
}
}
throw error;
}
}
/**
* Takes an ERC-4337 userOp object, and returns a classified transaction previewing what will happen if the userOp is executed.
*
* It includes an English description plus all relevant asset transfers tagged from the perspective of the userOp's 'sender' (the user).
* @param {string} chain - The chain name.
* @param {EVMTranslateUserOperation} userOperation - The ERC-4337 userOp object, in exactly the same format that would be submitted to a bundler for transaction execution.
* @param {number} block - OPTIONAL. The block number to preview the userOp at. Leave blank to preview the userOp in the current state of the chain.
* @returns {Promise<EVMForesightPreview4337Response>} A promise that resolves to the transaction preview details.
* @throws {TransactionError} If there are validation errors in the request.
*/
async preview4337(chain, userOperation, block) {
try {
let endpoint = `evm/${chain}/preview4337`;
endpoint += block ? `?block=${block}` : "";
const result = await this.request(endpoint, "POST", { body: JSON.stringify({ userOp: userOperation }) });
return result.response;
} catch (error) {
if (error instanceof Response) {
const errorResponse = await error.json();
if (errorResponse.status === 400 && errorResponse.errors) {
throw new TransactionError(errorResponse.errors);
}
}
throw error;
}
}
/**
* Returns a description of the action that will take place if the transaction executes.
*
* @param {string} chain - The chain name.
* @param {EVMTranslateUnsignedTransaction} unsignedTransaction - The unsigned transaction object, modeled after the standard format used by multiple EVM wallets.
* @returns {Promise<EVMForesightDescribeResponse>} A promise that resolves to the transaction description.
* @throws {TransactionError} If there are validation errors in the request.
*/
async describe(chain, unsignedTransaction) {
try {
let endpoint = `evm/${chain}/describe`;
const result = await this.request(endpoint, "POST", { body: JSON.stringify({ transaction: unsignedTransaction }) });
return result.response;
} catch (error) {
if (error instanceof Response) {
const errorResponse = await error.json();
if (errorResponse.status === 400 && errorResponse.errors) {
throw new TransactionError(errorResponse.errors);
}
}
throw error;
}
}
/**
* Validates that the response contains all required fields.
* @param {any} response - The response to validate.
* @param {string[]} requiredFields - Array of required field names.
* @returns {boolean} True if all required fields are present.
*/
validateResponse(response, requiredFields) {
return requiredFields.every((field) => response && typeof response[field] === "string");
}
/**