@adaptic/utils 0.0.979 → 0.0.980
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +454 -397
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +454 -398
- package/dist/index.mjs.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/massive.d.ts.map +1 -1
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/retry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3801,6 +3801,419 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
3801
3801
|
// Export the singleton instance
|
|
3802
3802
|
const marketDataAPI = AlpacaMarketDataAPI.getInstance();
|
|
3803
3803
|
|
|
3804
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
3805
|
+
maxRetries: 3,
|
|
3806
|
+
baseDelayMs: 1000,
|
|
3807
|
+
maxDelayMs: 30000,
|
|
3808
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
3809
|
+
retryOnNetworkError: true,
|
|
3810
|
+
};
|
|
3811
|
+
/**
|
|
3812
|
+
* Node.js / undici / system error codes that represent transient network
|
|
3813
|
+
* conditions. Present on `error.code` for net/http/dns/undici errors.
|
|
3814
|
+
*/
|
|
3815
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
3816
|
+
"ETIMEDOUT",
|
|
3817
|
+
"ESOCKETTIMEDOUT",
|
|
3818
|
+
"ECONNRESET",
|
|
3819
|
+
"ECONNREFUSED",
|
|
3820
|
+
"EHOSTUNREACH",
|
|
3821
|
+
"ENETUNREACH",
|
|
3822
|
+
"EAI_AGAIN",
|
|
3823
|
+
"EPIPE",
|
|
3824
|
+
"ECONNABORTED",
|
|
3825
|
+
"ENOTFOUND",
|
|
3826
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
3827
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
3828
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
3829
|
+
"UND_ERR_SOCKET",
|
|
3830
|
+
"UND_ERR_CLOSED",
|
|
3831
|
+
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
|
|
3832
|
+
]);
|
|
3833
|
+
/**
|
|
3834
|
+
* Error constructor names / `error.name` values that indicate transient
|
|
3835
|
+
* abort / timeout conditions.
|
|
3836
|
+
*/
|
|
3837
|
+
const RETRYABLE_ERROR_NAMES = new Set([
|
|
3838
|
+
"AbortError",
|
|
3839
|
+
"TimeoutError",
|
|
3840
|
+
"FetchError",
|
|
3841
|
+
"RequestTimeoutError",
|
|
3842
|
+
"ConnectTimeoutError",
|
|
3843
|
+
"HeadersTimeoutError",
|
|
3844
|
+
"BodyTimeoutError",
|
|
3845
|
+
]);
|
|
3846
|
+
/**
|
|
3847
|
+
* Message-pattern fallback for libraries that discard error codes/names but
|
|
3848
|
+
* preserve text (e.g., some Apollo/axios wrappers).
|
|
3849
|
+
*/
|
|
3850
|
+
const RETRYABLE_MESSAGE_PATTERNS = [
|
|
3851
|
+
/aborted/i,
|
|
3852
|
+
/timeout/i,
|
|
3853
|
+
/timed out/i,
|
|
3854
|
+
/network error/i,
|
|
3855
|
+
/socket hang up/i,
|
|
3856
|
+
/connection (reset|refused|closed)/i,
|
|
3857
|
+
/ECONNRESET/,
|
|
3858
|
+
/ETIMEDOUT/,
|
|
3859
|
+
/ECONNREFUSED/,
|
|
3860
|
+
/EAI_AGAIN/,
|
|
3861
|
+
/UND_ERR_/,
|
|
3862
|
+
];
|
|
3863
|
+
/**
|
|
3864
|
+
* Walks the `error.cause` chain (capped to avoid cycles) and tests whether
|
|
3865
|
+
* any link along the chain looks like a transient network error. Modern APIs
|
|
3866
|
+
* (undici, fetch, Apollo Client 3.8+) wrap the root network failure as a
|
|
3867
|
+
* `.cause`, so the surface `Error` may report a generic message while the
|
|
3868
|
+
* actionable signal lives one or more levels deeper.
|
|
3869
|
+
*
|
|
3870
|
+
* Exported for use by downstream consumers (engine services, per-call catch
|
|
3871
|
+
* blocks, application-level loggers) that need to demote recoverable
|
|
3872
|
+
* transient errors from ERROR to WARN. Aligns the whole stack on a single
|
|
3873
|
+
* canonical classifier so MassiveAPI, AlpacaAPI, and application-layer
|
|
3874
|
+
* retry handlers all treat the same network blips identically.
|
|
3875
|
+
*/
|
|
3876
|
+
function isTransientNetworkError(error) {
|
|
3877
|
+
const MAX_CAUSE_DEPTH = 6;
|
|
3878
|
+
let current = error;
|
|
3879
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
3880
|
+
if (current instanceof Error || typeof current === "object") {
|
|
3881
|
+
const err = current;
|
|
3882
|
+
if (typeof err.name === "string" && RETRYABLE_ERROR_NAMES.has(err.name)) {
|
|
3883
|
+
return true;
|
|
3884
|
+
}
|
|
3885
|
+
if (typeof err.code === "string" && RETRYABLE_ERROR_CODES.has(err.code)) {
|
|
3886
|
+
return true;
|
|
3887
|
+
}
|
|
3888
|
+
if (typeof err.message === "string") {
|
|
3889
|
+
for (const pattern of RETRYABLE_MESSAGE_PATTERNS) {
|
|
3890
|
+
if (pattern.test(err.message)) {
|
|
3891
|
+
return true;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
current = err.cause;
|
|
3896
|
+
}
|
|
3897
|
+
else {
|
|
3898
|
+
break;
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
return false;
|
|
3902
|
+
}
|
|
3903
|
+
/**
|
|
3904
|
+
* Analyzes an error and determines if it's retryable.
|
|
3905
|
+
* @param error - The error to analyze
|
|
3906
|
+
* @param response - Optional Response object for HTTP errors
|
|
3907
|
+
* @param config - Retry configuration
|
|
3908
|
+
* @returns Structured error details
|
|
3909
|
+
*/
|
|
3910
|
+
function analyzeError(error, response, config) {
|
|
3911
|
+
// Handle Response objects with error status codes
|
|
3912
|
+
if (response && !response.ok) {
|
|
3913
|
+
const status = response.status;
|
|
3914
|
+
// Rate limit errors - always retryable
|
|
3915
|
+
if (status === 429) {
|
|
3916
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
3917
|
+
const retryAfter = retryAfterHeader
|
|
3918
|
+
? parseInt(retryAfterHeader, 10) * 1000
|
|
3919
|
+
: undefined;
|
|
3920
|
+
return {
|
|
3921
|
+
type: "RATE_LIMIT",
|
|
3922
|
+
reason: "Rate limit exceeded",
|
|
3923
|
+
status,
|
|
3924
|
+
retryAfter,
|
|
3925
|
+
isRetryable: true,
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
// Authentication errors - never retry
|
|
3929
|
+
if (status === 401 || status === 403) {
|
|
3930
|
+
return {
|
|
3931
|
+
type: "AUTH_ERROR",
|
|
3932
|
+
reason: status === 401
|
|
3933
|
+
? "Authentication failed - invalid credentials"
|
|
3934
|
+
: "Access forbidden - insufficient permissions",
|
|
3935
|
+
status,
|
|
3936
|
+
isRetryable: false,
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
// Server errors - check if in retryable list
|
|
3940
|
+
if (status >= 500 && status < 600) {
|
|
3941
|
+
return {
|
|
3942
|
+
type: "SERVER_ERROR",
|
|
3943
|
+
reason: `Server error (${status})`,
|
|
3944
|
+
status,
|
|
3945
|
+
isRetryable: config.retryableStatusCodes.includes(status),
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
// Other client errors - never retry
|
|
3949
|
+
if (status >= 400 && status < 500) {
|
|
3950
|
+
return {
|
|
3951
|
+
type: "CLIENT_ERROR",
|
|
3952
|
+
reason: `Client error (${status})`,
|
|
3953
|
+
status,
|
|
3954
|
+
isRetryable: false,
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
// Handle network errors (TypeError from fetch API)
|
|
3959
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
3960
|
+
return {
|
|
3961
|
+
type: "NETWORK_ERROR",
|
|
3962
|
+
reason: "Network connectivity issue",
|
|
3963
|
+
status: null,
|
|
3964
|
+
isRetryable: config.retryOnNetworkError,
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
// Handle transient network conditions: AbortError, TimeoutError,
|
|
3968
|
+
// Node/undici error codes (ETIMEDOUT, ECONNRESET, UND_ERR_*), and
|
|
3969
|
+
// wrapped failures exposed via error.cause. This catches the broad class
|
|
3970
|
+
// of infrastructure flakes that the TypeError-only check above misses.
|
|
3971
|
+
if (isTransientNetworkError(error)) {
|
|
3972
|
+
const reason = error instanceof Error ? error.message : "Transient network error";
|
|
3973
|
+
return {
|
|
3974
|
+
type: "NETWORK_ERROR",
|
|
3975
|
+
reason,
|
|
3976
|
+
status: null,
|
|
3977
|
+
isRetryable: config.retryOnNetworkError,
|
|
3978
|
+
};
|
|
3979
|
+
}
|
|
3980
|
+
// Handle error objects with messages
|
|
3981
|
+
if (error instanceof Error) {
|
|
3982
|
+
// Parse error messages that might contain status information
|
|
3983
|
+
if (error.message.includes("429") || error.message.includes("RATE_LIMIT")) {
|
|
3984
|
+
const match = error.message.match(/RATE_LIMIT: 429:(\d+)/);
|
|
3985
|
+
const retryAfter = match ? parseInt(match[1], 10) : undefined;
|
|
3986
|
+
return {
|
|
3987
|
+
type: "RATE_LIMIT",
|
|
3988
|
+
reason: "Rate limit exceeded",
|
|
3989
|
+
status: 429,
|
|
3990
|
+
retryAfter,
|
|
3991
|
+
isRetryable: true,
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3994
|
+
if (error.message.includes("401") ||
|
|
3995
|
+
error.message.includes("403") ||
|
|
3996
|
+
error.message.includes("AUTH_ERROR")) {
|
|
3997
|
+
const status = error.message.includes("401") ? 401 : 403;
|
|
3998
|
+
return {
|
|
3999
|
+
type: "AUTH_ERROR",
|
|
4000
|
+
reason: `Authentication error (${status})`,
|
|
4001
|
+
status,
|
|
4002
|
+
isRetryable: false,
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
if (error.message.includes("SERVER_ERROR") ||
|
|
4006
|
+
error.message.match(/50[0-9]/)) {
|
|
4007
|
+
const statusMatch = error.message.match(/50[0-9]/);
|
|
4008
|
+
const status = statusMatch ? parseInt(statusMatch[0], 10) : 500;
|
|
4009
|
+
return {
|
|
4010
|
+
type: "SERVER_ERROR",
|
|
4011
|
+
reason: `Server error (${status})`,
|
|
4012
|
+
status,
|
|
4013
|
+
isRetryable: config.retryableStatusCodes.includes(status),
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
if (error.message.includes("network") ||
|
|
4017
|
+
error.message.includes("NETWORK_ERROR")) {
|
|
4018
|
+
return {
|
|
4019
|
+
type: "NETWORK_ERROR",
|
|
4020
|
+
reason: error.message,
|
|
4021
|
+
status: null,
|
|
4022
|
+
isRetryable: config.retryOnNetworkError,
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
// Unknown error - not retryable by default for safety
|
|
4027
|
+
return {
|
|
4028
|
+
type: "UNKNOWN",
|
|
4029
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
4030
|
+
status: null,
|
|
4031
|
+
isRetryable: false,
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Calculates the delay before the next retry attempt using exponential backoff with jitter.
|
|
4036
|
+
* @param attempt - Current attempt number (1-indexed)
|
|
4037
|
+
* @param baseDelay - Base delay in milliseconds
|
|
4038
|
+
* @param maxDelay - Maximum delay in milliseconds
|
|
4039
|
+
* @returns Delay in milliseconds
|
|
4040
|
+
*/
|
|
4041
|
+
function calculateBackoff(attempt, baseDelay, maxDelay) {
|
|
4042
|
+
// Exponential backoff: baseDelay * 2^(attempt-1)
|
|
4043
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
4044
|
+
// Cap at maxDelay
|
|
4045
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
4046
|
+
// Add jitter (random value between 0% and 25% of the delay)
|
|
4047
|
+
const jitter = Math.random() * cappedDelay * 0.25;
|
|
4048
|
+
return Math.floor(cappedDelay + jitter);
|
|
4049
|
+
}
|
|
4050
|
+
/**
|
|
4051
|
+
* Wraps an async function with retry logic and exponential backoff.
|
|
4052
|
+
*
|
|
4053
|
+
* This utility handles transient errors in external API calls by automatically retrying
|
|
4054
|
+
* failed requests with intelligent backoff strategies. It respects rate limit headers,
|
|
4055
|
+
* fails fast on non-retryable errors, and provides detailed logging.
|
|
4056
|
+
*
|
|
4057
|
+
* @template T - The return type of the wrapped function
|
|
4058
|
+
* @param fn - The async function to wrap with retry logic
|
|
4059
|
+
* @param config - Retry configuration (merged with defaults)
|
|
4060
|
+
* @param label - A descriptive label for logging (e.g., 'Massive.fetchTickerInfo')
|
|
4061
|
+
* @returns A promise that resolves to the function's return value
|
|
4062
|
+
* @throws The last error encountered if all retries are exhausted
|
|
4063
|
+
*
|
|
4064
|
+
* @example
|
|
4065
|
+
* ```typescript
|
|
4066
|
+
* // Basic usage with defaults
|
|
4067
|
+
* const data = await withRetry(
|
|
4068
|
+
* async () => fetch('https://api.example.com/data'),
|
|
4069
|
+
* {},
|
|
4070
|
+
* 'ExampleAPI.fetchData'
|
|
4071
|
+
* );
|
|
4072
|
+
*
|
|
4073
|
+
* // Custom configuration for rate-limited API
|
|
4074
|
+
* const result = await withRetry(
|
|
4075
|
+
* async () => alphaVantageAPI.getQuote(symbol),
|
|
4076
|
+
* {
|
|
4077
|
+
* maxRetries: 5,
|
|
4078
|
+
* baseDelayMs: 5000,
|
|
4079
|
+
* maxDelayMs: 60000,
|
|
4080
|
+
* onRetry: (attempt, error) => {
|
|
4081
|
+
* getLogger().info(`Retry ${attempt} after error:`, error);
|
|
4082
|
+
* }
|
|
4083
|
+
* },
|
|
4084
|
+
* 'AlphaVantage.getQuote'
|
|
4085
|
+
* );
|
|
4086
|
+
* ```
|
|
4087
|
+
*/
|
|
4088
|
+
async function withRetry(fn, config = {}, label = "unknown") {
|
|
4089
|
+
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
4090
|
+
let lastError;
|
|
4091
|
+
for (let attempt = 1; attempt <= fullConfig.maxRetries; attempt++) {
|
|
4092
|
+
try {
|
|
4093
|
+
const result = await fn();
|
|
4094
|
+
// If we succeeded after retries, log it
|
|
4095
|
+
if (attempt > 1) {
|
|
4096
|
+
getLogger().info(`[${label}] Succeeded on attempt ${attempt}/${fullConfig.maxRetries}`);
|
|
4097
|
+
}
|
|
4098
|
+
return result;
|
|
4099
|
+
}
|
|
4100
|
+
catch (error) {
|
|
4101
|
+
lastError = error;
|
|
4102
|
+
// If this is the last attempt, throw the error.
|
|
4103
|
+
// Transient network classes (undici/fetch timeouts, ECONNRESET,
|
|
4104
|
+
// AbortError, etc.) are self-healing at the upstream retry layer —
|
|
4105
|
+
// the caller re-invokes on the next refresh/poll tick. Logging them
|
|
4106
|
+
// at ERROR produces alert noise that does not represent actionable
|
|
4107
|
+
// failures. Demote the transient class to WARN with a recovery hint;
|
|
4108
|
+
// reserve ERROR for non-transient final failures (auth, schema,
|
|
4109
|
+
// contract violations, unknown classes).
|
|
4110
|
+
if (attempt === fullConfig.maxRetries) {
|
|
4111
|
+
const isTransient = isTransientNetworkError(error);
|
|
4112
|
+
const logMeta = {
|
|
4113
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4114
|
+
attempts: fullConfig.maxRetries,
|
|
4115
|
+
timestamp: new Date().toISOString(),
|
|
4116
|
+
...(isTransient
|
|
4117
|
+
? {
|
|
4118
|
+
transient: true,
|
|
4119
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
4120
|
+
}
|
|
4121
|
+
: {}),
|
|
4122
|
+
};
|
|
4123
|
+
if (isTransient) {
|
|
4124
|
+
getLogger().warn(`[${label}] Failed after ${fullConfig.maxRetries} attempts (transient)`, logMeta);
|
|
4125
|
+
}
|
|
4126
|
+
else {
|
|
4127
|
+
getLogger().error(`[${label}] Failed after ${fullConfig.maxRetries} attempts`, logMeta);
|
|
4128
|
+
}
|
|
4129
|
+
throw error;
|
|
4130
|
+
}
|
|
4131
|
+
// Analyze the error to determine if we should retry
|
|
4132
|
+
const response = error instanceof Response ? error : null;
|
|
4133
|
+
const errorDetails = analyzeError(error, response, fullConfig);
|
|
4134
|
+
// If error is not retryable, fail immediately
|
|
4135
|
+
if (!errorDetails.isRetryable) {
|
|
4136
|
+
getLogger().error(`[${label}] Non-retryable error (${errorDetails.type})`, {
|
|
4137
|
+
reason: errorDetails.reason,
|
|
4138
|
+
status: errorDetails.status,
|
|
4139
|
+
timestamp: new Date().toISOString(),
|
|
4140
|
+
});
|
|
4141
|
+
throw error;
|
|
4142
|
+
}
|
|
4143
|
+
// Calculate delay for next retry
|
|
4144
|
+
let delayMs;
|
|
4145
|
+
if (errorDetails.type === "RATE_LIMIT" && errorDetails.retryAfter) {
|
|
4146
|
+
// Use Retry-After header if available
|
|
4147
|
+
delayMs = errorDetails.retryAfter;
|
|
4148
|
+
}
|
|
4149
|
+
else if (errorDetails.type === "RATE_LIMIT") {
|
|
4150
|
+
// For rate limits without Retry-After, use a longer minimum delay
|
|
4151
|
+
delayMs = Math.max(calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs), 5000);
|
|
4152
|
+
}
|
|
4153
|
+
else {
|
|
4154
|
+
// Standard exponential backoff with jitter
|
|
4155
|
+
delayMs = calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs);
|
|
4156
|
+
}
|
|
4157
|
+
// Log the retry attempt
|
|
4158
|
+
getLogger().warn(`[${label}] Attempt ${attempt}/${fullConfig.maxRetries} failed: ${errorDetails.reason}. Retrying in ${delayMs}ms...`, {
|
|
4159
|
+
attemptNumber: attempt,
|
|
4160
|
+
totalRetries: fullConfig.maxRetries,
|
|
4161
|
+
errorType: errorDetails.type,
|
|
4162
|
+
httpStatus: errorDetails.status,
|
|
4163
|
+
retryDelay: delayMs,
|
|
4164
|
+
timestamp: new Date().toISOString(),
|
|
4165
|
+
});
|
|
4166
|
+
// Call the optional retry callback
|
|
4167
|
+
if (fullConfig.onRetry) {
|
|
4168
|
+
fullConfig.onRetry(attempt, error);
|
|
4169
|
+
}
|
|
4170
|
+
// Wait before retrying
|
|
4171
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
// This should never be reached due to the throw in the last attempt,
|
|
4175
|
+
// but TypeScript needs this to satisfy the return type
|
|
4176
|
+
throw lastError;
|
|
4177
|
+
}
|
|
4178
|
+
/**
|
|
4179
|
+
* API-specific retry configurations for different external services.
|
|
4180
|
+
* These configurations are tuned based on each API's rate limits and characteristics.
|
|
4181
|
+
*/
|
|
4182
|
+
const API_RETRY_CONFIGS = {
|
|
4183
|
+
/** Massive.com API - 5 requests/second rate limit */
|
|
4184
|
+
MASSIVE: {
|
|
4185
|
+
maxRetries: 3,
|
|
4186
|
+
baseDelayMs: 1000,
|
|
4187
|
+
maxDelayMs: 30000,
|
|
4188
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4189
|
+
retryOnNetworkError: true,
|
|
4190
|
+
},
|
|
4191
|
+
/** Alpha Vantage API - 5 requests/minute rate limit (more strict) */
|
|
4192
|
+
ALPHA_VANTAGE: {
|
|
4193
|
+
maxRetries: 5,
|
|
4194
|
+
baseDelayMs: 5000,
|
|
4195
|
+
maxDelayMs: 60000,
|
|
4196
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4197
|
+
retryOnNetworkError: true,
|
|
4198
|
+
},
|
|
4199
|
+
/** Alpaca API - generally reliable, shorter retry window */
|
|
4200
|
+
ALPACA: {
|
|
4201
|
+
maxRetries: 3,
|
|
4202
|
+
baseDelayMs: 1000,
|
|
4203
|
+
maxDelayMs: 30000,
|
|
4204
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4205
|
+
retryOnNetworkError: true,
|
|
4206
|
+
},
|
|
4207
|
+
/** Generic crypto API configuration */
|
|
4208
|
+
CRYPTO: {
|
|
4209
|
+
maxRetries: 3,
|
|
4210
|
+
baseDelayMs: 1000,
|
|
4211
|
+
maxDelayMs: 30000,
|
|
4212
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4213
|
+
retryOnNetworkError: true,
|
|
4214
|
+
},
|
|
4215
|
+
};
|
|
4216
|
+
|
|
3804
4217
|
const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
3805
4218
|
/**
|
|
3806
4219
|
Websocket example
|
|
@@ -4111,9 +4524,20 @@ class AlpacaTradingAPI {
|
|
|
4111
4524
|
}
|
|
4112
4525
|
catch (err) {
|
|
4113
4526
|
const error = err;
|
|
4527
|
+
const isTransient = isTransientNetworkError(err);
|
|
4528
|
+
// Transient fetch failures (timeouts, connection resets, undici
|
|
4529
|
+
// aborts) are recoverable at the caller's next cycle; log at WARN
|
|
4530
|
+
// and annotate for observability filters. Non-transient errors
|
|
4531
|
+
// (4xx/auth/schema) stay at ERROR for operator attention.
|
|
4114
4532
|
this.log(`Error in makeRequest: ${error.message}. Url: ${url}`, {
|
|
4115
4533
|
source: "AlpacaAPI",
|
|
4116
|
-
type: "error",
|
|
4534
|
+
type: isTransient ? "warn" : "error",
|
|
4535
|
+
metadata: isTransient
|
|
4536
|
+
? {
|
|
4537
|
+
transient: true,
|
|
4538
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
4539
|
+
}
|
|
4540
|
+
: undefined,
|
|
4117
4541
|
});
|
|
4118
4542
|
throw error;
|
|
4119
4543
|
}
|
|
@@ -5597,392 +6021,6 @@ async function createLimitOrder(auth, params = {
|
|
|
5597
6021
|
});
|
|
5598
6022
|
}
|
|
5599
6023
|
|
|
5600
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
5601
|
-
maxRetries: 3,
|
|
5602
|
-
baseDelayMs: 1000,
|
|
5603
|
-
maxDelayMs: 30000,
|
|
5604
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5605
|
-
retryOnNetworkError: true,
|
|
5606
|
-
};
|
|
5607
|
-
/**
|
|
5608
|
-
* Node.js / undici / system error codes that represent transient network
|
|
5609
|
-
* conditions. Present on `error.code` for net/http/dns/undici errors.
|
|
5610
|
-
*/
|
|
5611
|
-
const RETRYABLE_ERROR_CODES = new Set([
|
|
5612
|
-
"ETIMEDOUT",
|
|
5613
|
-
"ESOCKETTIMEDOUT",
|
|
5614
|
-
"ECONNRESET",
|
|
5615
|
-
"ECONNREFUSED",
|
|
5616
|
-
"EHOSTUNREACH",
|
|
5617
|
-
"ENETUNREACH",
|
|
5618
|
-
"EAI_AGAIN",
|
|
5619
|
-
"EPIPE",
|
|
5620
|
-
"ECONNABORTED",
|
|
5621
|
-
"ENOTFOUND",
|
|
5622
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
5623
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
5624
|
-
"UND_ERR_BODY_TIMEOUT",
|
|
5625
|
-
"UND_ERR_SOCKET",
|
|
5626
|
-
"UND_ERR_CLOSED",
|
|
5627
|
-
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
|
|
5628
|
-
]);
|
|
5629
|
-
/**
|
|
5630
|
-
* Error constructor names / `error.name` values that indicate transient
|
|
5631
|
-
* abort / timeout conditions.
|
|
5632
|
-
*/
|
|
5633
|
-
const RETRYABLE_ERROR_NAMES = new Set([
|
|
5634
|
-
"AbortError",
|
|
5635
|
-
"TimeoutError",
|
|
5636
|
-
"FetchError",
|
|
5637
|
-
"RequestTimeoutError",
|
|
5638
|
-
"ConnectTimeoutError",
|
|
5639
|
-
"HeadersTimeoutError",
|
|
5640
|
-
"BodyTimeoutError",
|
|
5641
|
-
]);
|
|
5642
|
-
/**
|
|
5643
|
-
* Message-pattern fallback for libraries that discard error codes/names but
|
|
5644
|
-
* preserve text (e.g., some Apollo/axios wrappers).
|
|
5645
|
-
*/
|
|
5646
|
-
const RETRYABLE_MESSAGE_PATTERNS = [
|
|
5647
|
-
/aborted/i,
|
|
5648
|
-
/timeout/i,
|
|
5649
|
-
/timed out/i,
|
|
5650
|
-
/network error/i,
|
|
5651
|
-
/socket hang up/i,
|
|
5652
|
-
/connection (reset|refused|closed)/i,
|
|
5653
|
-
/ECONNRESET/,
|
|
5654
|
-
/ETIMEDOUT/,
|
|
5655
|
-
/ECONNREFUSED/,
|
|
5656
|
-
/EAI_AGAIN/,
|
|
5657
|
-
/UND_ERR_/,
|
|
5658
|
-
];
|
|
5659
|
-
/**
|
|
5660
|
-
* Walks the `error.cause` chain (capped to avoid cycles) and tests whether
|
|
5661
|
-
* any link along the chain looks like a transient network error. Modern APIs
|
|
5662
|
-
* (undici, fetch, Apollo Client 3.8+) wrap the root network failure as a
|
|
5663
|
-
* `.cause`, so the surface `Error` may report a generic message while the
|
|
5664
|
-
* actionable signal lives one or more levels deeper.
|
|
5665
|
-
*/
|
|
5666
|
-
function isTransientNetworkError(error) {
|
|
5667
|
-
const MAX_CAUSE_DEPTH = 6;
|
|
5668
|
-
let current = error;
|
|
5669
|
-
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
5670
|
-
if (current instanceof Error || typeof current === "object") {
|
|
5671
|
-
const err = current;
|
|
5672
|
-
if (typeof err.name === "string" && RETRYABLE_ERROR_NAMES.has(err.name)) {
|
|
5673
|
-
return true;
|
|
5674
|
-
}
|
|
5675
|
-
if (typeof err.code === "string" && RETRYABLE_ERROR_CODES.has(err.code)) {
|
|
5676
|
-
return true;
|
|
5677
|
-
}
|
|
5678
|
-
if (typeof err.message === "string") {
|
|
5679
|
-
for (const pattern of RETRYABLE_MESSAGE_PATTERNS) {
|
|
5680
|
-
if (pattern.test(err.message)) {
|
|
5681
|
-
return true;
|
|
5682
|
-
}
|
|
5683
|
-
}
|
|
5684
|
-
}
|
|
5685
|
-
current = err.cause;
|
|
5686
|
-
}
|
|
5687
|
-
else {
|
|
5688
|
-
break;
|
|
5689
|
-
}
|
|
5690
|
-
}
|
|
5691
|
-
return false;
|
|
5692
|
-
}
|
|
5693
|
-
/**
|
|
5694
|
-
* Analyzes an error and determines if it's retryable.
|
|
5695
|
-
* @param error - The error to analyze
|
|
5696
|
-
* @param response - Optional Response object for HTTP errors
|
|
5697
|
-
* @param config - Retry configuration
|
|
5698
|
-
* @returns Structured error details
|
|
5699
|
-
*/
|
|
5700
|
-
function analyzeError(error, response, config) {
|
|
5701
|
-
// Handle Response objects with error status codes
|
|
5702
|
-
if (response && !response.ok) {
|
|
5703
|
-
const status = response.status;
|
|
5704
|
-
// Rate limit errors - always retryable
|
|
5705
|
-
if (status === 429) {
|
|
5706
|
-
const retryAfterHeader = response.headers.get("Retry-After");
|
|
5707
|
-
const retryAfter = retryAfterHeader
|
|
5708
|
-
? parseInt(retryAfterHeader, 10) * 1000
|
|
5709
|
-
: undefined;
|
|
5710
|
-
return {
|
|
5711
|
-
type: "RATE_LIMIT",
|
|
5712
|
-
reason: "Rate limit exceeded",
|
|
5713
|
-
status,
|
|
5714
|
-
retryAfter,
|
|
5715
|
-
isRetryable: true,
|
|
5716
|
-
};
|
|
5717
|
-
}
|
|
5718
|
-
// Authentication errors - never retry
|
|
5719
|
-
if (status === 401 || status === 403) {
|
|
5720
|
-
return {
|
|
5721
|
-
type: "AUTH_ERROR",
|
|
5722
|
-
reason: status === 401
|
|
5723
|
-
? "Authentication failed - invalid credentials"
|
|
5724
|
-
: "Access forbidden - insufficient permissions",
|
|
5725
|
-
status,
|
|
5726
|
-
isRetryable: false,
|
|
5727
|
-
};
|
|
5728
|
-
}
|
|
5729
|
-
// Server errors - check if in retryable list
|
|
5730
|
-
if (status >= 500 && status < 600) {
|
|
5731
|
-
return {
|
|
5732
|
-
type: "SERVER_ERROR",
|
|
5733
|
-
reason: `Server error (${status})`,
|
|
5734
|
-
status,
|
|
5735
|
-
isRetryable: config.retryableStatusCodes.includes(status),
|
|
5736
|
-
};
|
|
5737
|
-
}
|
|
5738
|
-
// Other client errors - never retry
|
|
5739
|
-
if (status >= 400 && status < 500) {
|
|
5740
|
-
return {
|
|
5741
|
-
type: "CLIENT_ERROR",
|
|
5742
|
-
reason: `Client error (${status})`,
|
|
5743
|
-
status,
|
|
5744
|
-
isRetryable: false,
|
|
5745
|
-
};
|
|
5746
|
-
}
|
|
5747
|
-
}
|
|
5748
|
-
// Handle network errors (TypeError from fetch API)
|
|
5749
|
-
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
5750
|
-
return {
|
|
5751
|
-
type: "NETWORK_ERROR",
|
|
5752
|
-
reason: "Network connectivity issue",
|
|
5753
|
-
status: null,
|
|
5754
|
-
isRetryable: config.retryOnNetworkError,
|
|
5755
|
-
};
|
|
5756
|
-
}
|
|
5757
|
-
// Handle transient network conditions: AbortError, TimeoutError,
|
|
5758
|
-
// Node/undici error codes (ETIMEDOUT, ECONNRESET, UND_ERR_*), and
|
|
5759
|
-
// wrapped failures exposed via error.cause. This catches the broad class
|
|
5760
|
-
// of infrastructure flakes that the TypeError-only check above misses.
|
|
5761
|
-
if (isTransientNetworkError(error)) {
|
|
5762
|
-
const reason = error instanceof Error ? error.message : "Transient network error";
|
|
5763
|
-
return {
|
|
5764
|
-
type: "NETWORK_ERROR",
|
|
5765
|
-
reason,
|
|
5766
|
-
status: null,
|
|
5767
|
-
isRetryable: config.retryOnNetworkError,
|
|
5768
|
-
};
|
|
5769
|
-
}
|
|
5770
|
-
// Handle error objects with messages
|
|
5771
|
-
if (error instanceof Error) {
|
|
5772
|
-
// Parse error messages that might contain status information
|
|
5773
|
-
if (error.message.includes("429") || error.message.includes("RATE_LIMIT")) {
|
|
5774
|
-
const match = error.message.match(/RATE_LIMIT: 429:(\d+)/);
|
|
5775
|
-
const retryAfter = match ? parseInt(match[1], 10) : undefined;
|
|
5776
|
-
return {
|
|
5777
|
-
type: "RATE_LIMIT",
|
|
5778
|
-
reason: "Rate limit exceeded",
|
|
5779
|
-
status: 429,
|
|
5780
|
-
retryAfter,
|
|
5781
|
-
isRetryable: true,
|
|
5782
|
-
};
|
|
5783
|
-
}
|
|
5784
|
-
if (error.message.includes("401") ||
|
|
5785
|
-
error.message.includes("403") ||
|
|
5786
|
-
error.message.includes("AUTH_ERROR")) {
|
|
5787
|
-
const status = error.message.includes("401") ? 401 : 403;
|
|
5788
|
-
return {
|
|
5789
|
-
type: "AUTH_ERROR",
|
|
5790
|
-
reason: `Authentication error (${status})`,
|
|
5791
|
-
status,
|
|
5792
|
-
isRetryable: false,
|
|
5793
|
-
};
|
|
5794
|
-
}
|
|
5795
|
-
if (error.message.includes("SERVER_ERROR") ||
|
|
5796
|
-
error.message.match(/50[0-9]/)) {
|
|
5797
|
-
const statusMatch = error.message.match(/50[0-9]/);
|
|
5798
|
-
const status = statusMatch ? parseInt(statusMatch[0], 10) : 500;
|
|
5799
|
-
return {
|
|
5800
|
-
type: "SERVER_ERROR",
|
|
5801
|
-
reason: `Server error (${status})`,
|
|
5802
|
-
status,
|
|
5803
|
-
isRetryable: config.retryableStatusCodes.includes(status),
|
|
5804
|
-
};
|
|
5805
|
-
}
|
|
5806
|
-
if (error.message.includes("network") ||
|
|
5807
|
-
error.message.includes("NETWORK_ERROR")) {
|
|
5808
|
-
return {
|
|
5809
|
-
type: "NETWORK_ERROR",
|
|
5810
|
-
reason: error.message,
|
|
5811
|
-
status: null,
|
|
5812
|
-
isRetryable: config.retryOnNetworkError,
|
|
5813
|
-
};
|
|
5814
|
-
}
|
|
5815
|
-
}
|
|
5816
|
-
// Unknown error - not retryable by default for safety
|
|
5817
|
-
return {
|
|
5818
|
-
type: "UNKNOWN",
|
|
5819
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
5820
|
-
status: null,
|
|
5821
|
-
isRetryable: false,
|
|
5822
|
-
};
|
|
5823
|
-
}
|
|
5824
|
-
/**
|
|
5825
|
-
* Calculates the delay before the next retry attempt using exponential backoff with jitter.
|
|
5826
|
-
* @param attempt - Current attempt number (1-indexed)
|
|
5827
|
-
* @param baseDelay - Base delay in milliseconds
|
|
5828
|
-
* @param maxDelay - Maximum delay in milliseconds
|
|
5829
|
-
* @returns Delay in milliseconds
|
|
5830
|
-
*/
|
|
5831
|
-
function calculateBackoff(attempt, baseDelay, maxDelay) {
|
|
5832
|
-
// Exponential backoff: baseDelay * 2^(attempt-1)
|
|
5833
|
-
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
5834
|
-
// Cap at maxDelay
|
|
5835
|
-
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
5836
|
-
// Add jitter (random value between 0% and 25% of the delay)
|
|
5837
|
-
const jitter = Math.random() * cappedDelay * 0.25;
|
|
5838
|
-
return Math.floor(cappedDelay + jitter);
|
|
5839
|
-
}
|
|
5840
|
-
/**
|
|
5841
|
-
* Wraps an async function with retry logic and exponential backoff.
|
|
5842
|
-
*
|
|
5843
|
-
* This utility handles transient errors in external API calls by automatically retrying
|
|
5844
|
-
* failed requests with intelligent backoff strategies. It respects rate limit headers,
|
|
5845
|
-
* fails fast on non-retryable errors, and provides detailed logging.
|
|
5846
|
-
*
|
|
5847
|
-
* @template T - The return type of the wrapped function
|
|
5848
|
-
* @param fn - The async function to wrap with retry logic
|
|
5849
|
-
* @param config - Retry configuration (merged with defaults)
|
|
5850
|
-
* @param label - A descriptive label for logging (e.g., 'Massive.fetchTickerInfo')
|
|
5851
|
-
* @returns A promise that resolves to the function's return value
|
|
5852
|
-
* @throws The last error encountered if all retries are exhausted
|
|
5853
|
-
*
|
|
5854
|
-
* @example
|
|
5855
|
-
* ```typescript
|
|
5856
|
-
* // Basic usage with defaults
|
|
5857
|
-
* const data = await withRetry(
|
|
5858
|
-
* async () => fetch('https://api.example.com/data'),
|
|
5859
|
-
* {},
|
|
5860
|
-
* 'ExampleAPI.fetchData'
|
|
5861
|
-
* );
|
|
5862
|
-
*
|
|
5863
|
-
* // Custom configuration for rate-limited API
|
|
5864
|
-
* const result = await withRetry(
|
|
5865
|
-
* async () => alphaVantageAPI.getQuote(symbol),
|
|
5866
|
-
* {
|
|
5867
|
-
* maxRetries: 5,
|
|
5868
|
-
* baseDelayMs: 5000,
|
|
5869
|
-
* maxDelayMs: 60000,
|
|
5870
|
-
* onRetry: (attempt, error) => {
|
|
5871
|
-
* getLogger().info(`Retry ${attempt} after error:`, error);
|
|
5872
|
-
* }
|
|
5873
|
-
* },
|
|
5874
|
-
* 'AlphaVantage.getQuote'
|
|
5875
|
-
* );
|
|
5876
|
-
* ```
|
|
5877
|
-
*/
|
|
5878
|
-
async function withRetry(fn, config = {}, label = "unknown") {
|
|
5879
|
-
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
5880
|
-
let lastError;
|
|
5881
|
-
for (let attempt = 1; attempt <= fullConfig.maxRetries; attempt++) {
|
|
5882
|
-
try {
|
|
5883
|
-
const result = await fn();
|
|
5884
|
-
// If we succeeded after retries, log it
|
|
5885
|
-
if (attempt > 1) {
|
|
5886
|
-
getLogger().info(`[${label}] Succeeded on attempt ${attempt}/${fullConfig.maxRetries}`);
|
|
5887
|
-
}
|
|
5888
|
-
return result;
|
|
5889
|
-
}
|
|
5890
|
-
catch (error) {
|
|
5891
|
-
lastError = error;
|
|
5892
|
-
// If this is the last attempt, throw the error
|
|
5893
|
-
if (attempt === fullConfig.maxRetries) {
|
|
5894
|
-
getLogger().error(`[${label}] Failed after ${fullConfig.maxRetries} attempts`, {
|
|
5895
|
-
error: error instanceof Error ? error.message : String(error),
|
|
5896
|
-
timestamp: new Date().toISOString(),
|
|
5897
|
-
});
|
|
5898
|
-
throw error;
|
|
5899
|
-
}
|
|
5900
|
-
// Analyze the error to determine if we should retry
|
|
5901
|
-
const response = error instanceof Response ? error : null;
|
|
5902
|
-
const errorDetails = analyzeError(error, response, fullConfig);
|
|
5903
|
-
// If error is not retryable, fail immediately
|
|
5904
|
-
if (!errorDetails.isRetryable) {
|
|
5905
|
-
getLogger().error(`[${label}] Non-retryable error (${errorDetails.type})`, {
|
|
5906
|
-
reason: errorDetails.reason,
|
|
5907
|
-
status: errorDetails.status,
|
|
5908
|
-
timestamp: new Date().toISOString(),
|
|
5909
|
-
});
|
|
5910
|
-
throw error;
|
|
5911
|
-
}
|
|
5912
|
-
// Calculate delay for next retry
|
|
5913
|
-
let delayMs;
|
|
5914
|
-
if (errorDetails.type === "RATE_LIMIT" && errorDetails.retryAfter) {
|
|
5915
|
-
// Use Retry-After header if available
|
|
5916
|
-
delayMs = errorDetails.retryAfter;
|
|
5917
|
-
}
|
|
5918
|
-
else if (errorDetails.type === "RATE_LIMIT") {
|
|
5919
|
-
// For rate limits without Retry-After, use a longer minimum delay
|
|
5920
|
-
delayMs = Math.max(calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs), 5000);
|
|
5921
|
-
}
|
|
5922
|
-
else {
|
|
5923
|
-
// Standard exponential backoff with jitter
|
|
5924
|
-
delayMs = calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs);
|
|
5925
|
-
}
|
|
5926
|
-
// Log the retry attempt
|
|
5927
|
-
getLogger().warn(`[${label}] Attempt ${attempt}/${fullConfig.maxRetries} failed: ${errorDetails.reason}. Retrying in ${delayMs}ms...`, {
|
|
5928
|
-
attemptNumber: attempt,
|
|
5929
|
-
totalRetries: fullConfig.maxRetries,
|
|
5930
|
-
errorType: errorDetails.type,
|
|
5931
|
-
httpStatus: errorDetails.status,
|
|
5932
|
-
retryDelay: delayMs,
|
|
5933
|
-
timestamp: new Date().toISOString(),
|
|
5934
|
-
});
|
|
5935
|
-
// Call the optional retry callback
|
|
5936
|
-
if (fullConfig.onRetry) {
|
|
5937
|
-
fullConfig.onRetry(attempt, error);
|
|
5938
|
-
}
|
|
5939
|
-
// Wait before retrying
|
|
5940
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
5941
|
-
}
|
|
5942
|
-
}
|
|
5943
|
-
// This should never be reached due to the throw in the last attempt,
|
|
5944
|
-
// but TypeScript needs this to satisfy the return type
|
|
5945
|
-
throw lastError;
|
|
5946
|
-
}
|
|
5947
|
-
/**
|
|
5948
|
-
* API-specific retry configurations for different external services.
|
|
5949
|
-
* These configurations are tuned based on each API's rate limits and characteristics.
|
|
5950
|
-
*/
|
|
5951
|
-
const API_RETRY_CONFIGS = {
|
|
5952
|
-
/** Massive.com API - 5 requests/second rate limit */
|
|
5953
|
-
MASSIVE: {
|
|
5954
|
-
maxRetries: 3,
|
|
5955
|
-
baseDelayMs: 1000,
|
|
5956
|
-
maxDelayMs: 30000,
|
|
5957
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5958
|
-
retryOnNetworkError: true,
|
|
5959
|
-
},
|
|
5960
|
-
/** Alpha Vantage API - 5 requests/minute rate limit (more strict) */
|
|
5961
|
-
ALPHA_VANTAGE: {
|
|
5962
|
-
maxRetries: 5,
|
|
5963
|
-
baseDelayMs: 5000,
|
|
5964
|
-
maxDelayMs: 60000,
|
|
5965
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5966
|
-
retryOnNetworkError: true,
|
|
5967
|
-
},
|
|
5968
|
-
/** Alpaca API - generally reliable, shorter retry window */
|
|
5969
|
-
ALPACA: {
|
|
5970
|
-
maxRetries: 3,
|
|
5971
|
-
baseDelayMs: 1000,
|
|
5972
|
-
maxDelayMs: 30000,
|
|
5973
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5974
|
-
retryOnNetworkError: true,
|
|
5975
|
-
},
|
|
5976
|
-
/** Generic crypto API configuration */
|
|
5977
|
-
CRYPTO: {
|
|
5978
|
-
maxRetries: 3,
|
|
5979
|
-
baseDelayMs: 1000,
|
|
5980
|
-
maxDelayMs: 30000,
|
|
5981
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5982
|
-
retryOnNetworkError: true,
|
|
5983
|
-
},
|
|
5984
|
-
};
|
|
5985
|
-
|
|
5986
6024
|
// Utility function for debug logging
|
|
5987
6025
|
/**
|
|
5988
6026
|
* Debug logging utility that respects environment debug flags.
|
|
@@ -8509,19 +8547,37 @@ const fetchPrices = async (params, options) => {
|
|
|
8509
8547
|
catch (error) {
|
|
8510
8548
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
8511
8549
|
const contextualMessage = `Error fetching price data for ${ticker}`;
|
|
8512
|
-
|
|
8550
|
+
const isTransient = isTransientNetworkError(error);
|
|
8551
|
+
const errorType = error instanceof Error && error.message.includes("AUTH_ERROR")
|
|
8552
|
+
? "AUTH_ERROR"
|
|
8553
|
+
: error instanceof Error && error.message.includes("RATE_LIMIT")
|
|
8554
|
+
? "RATE_LIMIT"
|
|
8555
|
+
: isTransient
|
|
8556
|
+
? "NETWORK_ERROR"
|
|
8557
|
+
: "UNKNOWN";
|
|
8558
|
+
// Transient network failures (undici timeouts, socket resets, DNS
|
|
8559
|
+
// blips) are recoverable at the caller's next cycle — demote to
|
|
8560
|
+
// WARN to avoid alert noise. Non-transient classes (auth, rate
|
|
8561
|
+
// limits that have exhausted retries, unknown schema errors) stay
|
|
8562
|
+
// at ERROR because they require operator attention.
|
|
8563
|
+
const logMeta = {
|
|
8513
8564
|
ticker,
|
|
8514
|
-
errorType
|
|
8515
|
-
? "AUTH_ERROR"
|
|
8516
|
-
: error instanceof Error && error.message.includes("RATE_LIMIT")
|
|
8517
|
-
? "RATE_LIMIT"
|
|
8518
|
-
: error instanceof Error &&
|
|
8519
|
-
error.message.includes("NETWORK_ERROR")
|
|
8520
|
-
? "NETWORK_ERROR"
|
|
8521
|
-
: "UNKNOWN",
|
|
8565
|
+
errorType,
|
|
8522
8566
|
source: "MassiveAPI.fetchPrices",
|
|
8523
8567
|
timestamp: new Date().toISOString(),
|
|
8524
|
-
|
|
8568
|
+
...(isTransient
|
|
8569
|
+
? {
|
|
8570
|
+
transient: true,
|
|
8571
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
8572
|
+
}
|
|
8573
|
+
: {}),
|
|
8574
|
+
};
|
|
8575
|
+
if (isTransient) {
|
|
8576
|
+
getLogger().warn(`${contextualMessage}: ${errorMessage}`, logMeta);
|
|
8577
|
+
}
|
|
8578
|
+
else {
|
|
8579
|
+
getLogger().error(`${contextualMessage}: ${errorMessage}`, logMeta);
|
|
8580
|
+
}
|
|
8525
8581
|
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
8526
8582
|
}
|
|
8527
8583
|
});
|
|
@@ -68626,6 +68682,7 @@ exports.isOrderFilled = isOrderFilled;
|
|
|
68626
68682
|
exports.isOrderOpen = isOrderOpen;
|
|
68627
68683
|
exports.isOrderTerminalStatus = isOrderTerminal$1;
|
|
68628
68684
|
exports.isSupportedCryptoPair = isSupportedCryptoPair;
|
|
68685
|
+
exports.isTransientNetworkError = isTransientNetworkError;
|
|
68629
68686
|
exports.legacyApi = index$1;
|
|
68630
68687
|
exports.limitBuyWithTakeProfit = limitBuyWithTakeProfit;
|
|
68631
68688
|
exports.ocoOrders = ocoOrders;
|