ton-provider-system 0.2.2 → 0.2.4
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/README.md +111 -14
- package/dist/index.cjs +189 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +189 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -875,7 +875,10 @@ declare class TokenBucketRateLimiter {
|
|
|
875
875
|
*/
|
|
876
876
|
release(): void;
|
|
877
877
|
/**
|
|
878
|
-
* Report a successful request (
|
|
878
|
+
* Report a successful request (gradually reduces backoff)
|
|
879
|
+
*
|
|
880
|
+
* Instead of immediately clearing backoff, we gradually reduce it to prevent
|
|
881
|
+
* immediately hitting the rate limit again after a single success.
|
|
879
882
|
*/
|
|
880
883
|
reportSuccess(): void;
|
|
881
884
|
/**
|
|
@@ -1004,6 +1007,7 @@ declare class HealthChecker {
|
|
|
1004
1007
|
*
|
|
1005
1008
|
* @param batchSize - Number of providers to test in parallel (default: 2)
|
|
1006
1009
|
* @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
|
|
1010
|
+
* If not provided, calculates delay based on lowest RPS in batch
|
|
1007
1011
|
*/
|
|
1008
1012
|
testProviders(providers: ResolvedProvider[], batchSize?: number, batchDelayMs?: number): Promise<ProviderHealthResult[]>;
|
|
1009
1013
|
/**
|
|
@@ -1084,6 +1088,8 @@ interface SelectionConfig {
|
|
|
1084
1088
|
freshnessWeight: number;
|
|
1085
1089
|
/** Minimum acceptable provider status */
|
|
1086
1090
|
minStatus: ProviderStatus[];
|
|
1091
|
+
/** Cooldown period (ms) before retrying failed providers (default: 30000) */
|
|
1092
|
+
retryCooldownMs: number;
|
|
1087
1093
|
}
|
|
1088
1094
|
/**
|
|
1089
1095
|
* Provider Selector
|
|
@@ -1196,6 +1202,22 @@ declare class ProviderSelector {
|
|
|
1196
1202
|
* 2. Health check result browserCompatible flag (if health check was performed)
|
|
1197
1203
|
*/
|
|
1198
1204
|
private filterBrowserCompatible;
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if an error is retryable (temporary) or permanent
|
|
1207
|
+
*
|
|
1208
|
+
* Permanent errors (never retry):
|
|
1209
|
+
* - 404 (Not Found) - endpoint doesn't exist
|
|
1210
|
+
* - 401 (Unauthorized) - invalid API key
|
|
1211
|
+
* - Invalid API key errors
|
|
1212
|
+
*
|
|
1213
|
+
* Retryable errors (can retry after cooldown):
|
|
1214
|
+
* - 503 (Service Unavailable) - temporary server issue
|
|
1215
|
+
* - 502 (Bad Gateway) - temporary gateway issue
|
|
1216
|
+
* - Timeout errors - network issues
|
|
1217
|
+
* - Network errors - connection issues
|
|
1218
|
+
* - 429 (Rate Limit) - temporary rate limit (handled separately)
|
|
1219
|
+
*/
|
|
1220
|
+
private isRetryableError;
|
|
1199
1221
|
}
|
|
1200
1222
|
/**
|
|
1201
1223
|
* Create a provider selector
|
|
@@ -1554,18 +1576,26 @@ declare class BrowserAdapter {
|
|
|
1554
1576
|
getEndpointWithRateLimit(timeoutMs?: number): Promise<string>;
|
|
1555
1577
|
/**
|
|
1556
1578
|
* Make a JSON-RPC call to the TON API
|
|
1579
|
+
*
|
|
1580
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1557
1581
|
*/
|
|
1558
1582
|
jsonRpc<T = unknown>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
1559
1583
|
/**
|
|
1560
1584
|
* Get address state
|
|
1585
|
+
*
|
|
1586
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1561
1587
|
*/
|
|
1562
1588
|
getAddressState(address: string, timeoutMs?: number): Promise<'uninit' | 'active' | 'frozen'>;
|
|
1563
1589
|
/**
|
|
1564
1590
|
* Get address balance
|
|
1591
|
+
*
|
|
1592
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1565
1593
|
*/
|
|
1566
1594
|
getAddressBalance(address: string, timeoutMs?: number): Promise<bigint>;
|
|
1567
1595
|
/**
|
|
1568
1596
|
* Get address information
|
|
1597
|
+
*
|
|
1598
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1569
1599
|
*/
|
|
1570
1600
|
getAddressInfo(address: string, timeoutMs?: number): Promise<{
|
|
1571
1601
|
state: 'uninit' | 'active' | 'frozen';
|
|
@@ -1575,6 +1605,8 @@ declare class BrowserAdapter {
|
|
|
1575
1605
|
}>;
|
|
1576
1606
|
/**
|
|
1577
1607
|
* Run get method
|
|
1608
|
+
*
|
|
1609
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1578
1610
|
*/
|
|
1579
1611
|
runGetMethod(address: string, method: string, stack?: unknown[], timeoutMs?: number): Promise<{
|
|
1580
1612
|
exit_code: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -875,7 +875,10 @@ declare class TokenBucketRateLimiter {
|
|
|
875
875
|
*/
|
|
876
876
|
release(): void;
|
|
877
877
|
/**
|
|
878
|
-
* Report a successful request (
|
|
878
|
+
* Report a successful request (gradually reduces backoff)
|
|
879
|
+
*
|
|
880
|
+
* Instead of immediately clearing backoff, we gradually reduce it to prevent
|
|
881
|
+
* immediately hitting the rate limit again after a single success.
|
|
879
882
|
*/
|
|
880
883
|
reportSuccess(): void;
|
|
881
884
|
/**
|
|
@@ -1004,6 +1007,7 @@ declare class HealthChecker {
|
|
|
1004
1007
|
*
|
|
1005
1008
|
* @param batchSize - Number of providers to test in parallel (default: 2)
|
|
1006
1009
|
* @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
|
|
1010
|
+
* If not provided, calculates delay based on lowest RPS in batch
|
|
1007
1011
|
*/
|
|
1008
1012
|
testProviders(providers: ResolvedProvider[], batchSize?: number, batchDelayMs?: number): Promise<ProviderHealthResult[]>;
|
|
1009
1013
|
/**
|
|
@@ -1084,6 +1088,8 @@ interface SelectionConfig {
|
|
|
1084
1088
|
freshnessWeight: number;
|
|
1085
1089
|
/** Minimum acceptable provider status */
|
|
1086
1090
|
minStatus: ProviderStatus[];
|
|
1091
|
+
/** Cooldown period (ms) before retrying failed providers (default: 30000) */
|
|
1092
|
+
retryCooldownMs: number;
|
|
1087
1093
|
}
|
|
1088
1094
|
/**
|
|
1089
1095
|
* Provider Selector
|
|
@@ -1196,6 +1202,22 @@ declare class ProviderSelector {
|
|
|
1196
1202
|
* 2. Health check result browserCompatible flag (if health check was performed)
|
|
1197
1203
|
*/
|
|
1198
1204
|
private filterBrowserCompatible;
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if an error is retryable (temporary) or permanent
|
|
1207
|
+
*
|
|
1208
|
+
* Permanent errors (never retry):
|
|
1209
|
+
* - 404 (Not Found) - endpoint doesn't exist
|
|
1210
|
+
* - 401 (Unauthorized) - invalid API key
|
|
1211
|
+
* - Invalid API key errors
|
|
1212
|
+
*
|
|
1213
|
+
* Retryable errors (can retry after cooldown):
|
|
1214
|
+
* - 503 (Service Unavailable) - temporary server issue
|
|
1215
|
+
* - 502 (Bad Gateway) - temporary gateway issue
|
|
1216
|
+
* - Timeout errors - network issues
|
|
1217
|
+
* - Network errors - connection issues
|
|
1218
|
+
* - 429 (Rate Limit) - temporary rate limit (handled separately)
|
|
1219
|
+
*/
|
|
1220
|
+
private isRetryableError;
|
|
1199
1221
|
}
|
|
1200
1222
|
/**
|
|
1201
1223
|
* Create a provider selector
|
|
@@ -1554,18 +1576,26 @@ declare class BrowserAdapter {
|
|
|
1554
1576
|
getEndpointWithRateLimit(timeoutMs?: number): Promise<string>;
|
|
1555
1577
|
/**
|
|
1556
1578
|
* Make a JSON-RPC call to the TON API
|
|
1579
|
+
*
|
|
1580
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1557
1581
|
*/
|
|
1558
1582
|
jsonRpc<T = unknown>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
1559
1583
|
/**
|
|
1560
1584
|
* Get address state
|
|
1585
|
+
*
|
|
1586
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1561
1587
|
*/
|
|
1562
1588
|
getAddressState(address: string, timeoutMs?: number): Promise<'uninit' | 'active' | 'frozen'>;
|
|
1563
1589
|
/**
|
|
1564
1590
|
* Get address balance
|
|
1591
|
+
*
|
|
1592
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1565
1593
|
*/
|
|
1566
1594
|
getAddressBalance(address: string, timeoutMs?: number): Promise<bigint>;
|
|
1567
1595
|
/**
|
|
1568
1596
|
* Get address information
|
|
1597
|
+
*
|
|
1598
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1569
1599
|
*/
|
|
1570
1600
|
getAddressInfo(address: string, timeoutMs?: number): Promise<{
|
|
1571
1601
|
state: 'uninit' | 'active' | 'frozen';
|
|
@@ -1575,6 +1605,8 @@ declare class BrowserAdapter {
|
|
|
1575
1605
|
}>;
|
|
1576
1606
|
/**
|
|
1577
1607
|
* Run get method
|
|
1608
|
+
*
|
|
1609
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
1578
1610
|
*/
|
|
1579
1611
|
runGetMethod(address: string, method: string, stack?: unknown[], timeoutMs?: number): Promise<{
|
|
1580
1612
|
exit_code: number;
|
package/dist/index.js
CHANGED
|
@@ -1154,17 +1154,26 @@ var HealthChecker = class {
|
|
|
1154
1154
|
const endTime = performance.now();
|
|
1155
1155
|
const latencyMs = Math.round(endTime - startTime);
|
|
1156
1156
|
const errorMsg = error.message || String(error) || "Unknown error";
|
|
1157
|
+
const errorMsgLower = errorMsg.toLowerCase();
|
|
1157
1158
|
const isCorsError = this.isCorsError(error, errorMsg);
|
|
1158
|
-
const
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
const
|
|
1159
|
+
const responseStatus = error?.response?.status || error?.status || error?.statusCode || null;
|
|
1160
|
+
const statusMatch = errorMsg.match(/\b(\d{3})\b/);
|
|
1161
|
+
const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
1162
|
+
const httpStatus = responseStatus || statusFromMsg;
|
|
1163
|
+
const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
|
|
1164
|
+
const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
|
|
1165
|
+
const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
|
|
1166
|
+
const is403 = httpStatus === 403 || errorMsgLower.includes("403") || errorMsgLower.includes("forbidden");
|
|
1167
|
+
const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
|
|
1168
|
+
const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
|
|
1169
|
+
const isTimeout = error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("aborted");
|
|
1170
|
+
const isOnFinalityBackendError = provider.type === "onfinality" && (errorMsgLower.includes("backend error") || errorMsgLower.includes("backend error"));
|
|
1164
1171
|
let status = "offline";
|
|
1165
1172
|
if (is429) {
|
|
1166
1173
|
status = "degraded";
|
|
1167
|
-
} else if (is404 ||
|
|
1174
|
+
} else if (is404 || is401 || is403) {
|
|
1175
|
+
status = "offline";
|
|
1176
|
+
} else if (is503 || is502 || isOnFinalityBackendError) {
|
|
1168
1177
|
status = "offline";
|
|
1169
1178
|
} else if (isTimeout) {
|
|
1170
1179
|
status = "offline";
|
|
@@ -1192,8 +1201,9 @@ var HealthChecker = class {
|
|
|
1192
1201
|
*
|
|
1193
1202
|
* @param batchSize - Number of providers to test in parallel (default: 2)
|
|
1194
1203
|
* @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
|
|
1204
|
+
* If not provided, calculates delay based on lowest RPS in batch
|
|
1195
1205
|
*/
|
|
1196
|
-
async testProviders(providers, batchSize = 2, batchDelayMs
|
|
1206
|
+
async testProviders(providers, batchSize = 2, batchDelayMs) {
|
|
1197
1207
|
const results = [];
|
|
1198
1208
|
for (let i = 0; i < providers.length; i += batchSize) {
|
|
1199
1209
|
const batch = providers.slice(i, i + batchSize);
|
|
@@ -1201,8 +1211,15 @@ var HealthChecker = class {
|
|
|
1201
1211
|
batch.map((p) => this.testProvider(p))
|
|
1202
1212
|
);
|
|
1203
1213
|
results.push(...batchResults);
|
|
1204
|
-
if (i + batchSize < providers.length
|
|
1205
|
-
|
|
1214
|
+
if (i + batchSize < providers.length) {
|
|
1215
|
+
let delay = batchDelayMs;
|
|
1216
|
+
if (delay === void 0) {
|
|
1217
|
+
const minRps = Math.min(...batch.map((p) => p.rps || 1));
|
|
1218
|
+
delay = Math.max(500, Math.ceil(1e3 / minRps * 1.5));
|
|
1219
|
+
}
|
|
1220
|
+
if (delay > 0) {
|
|
1221
|
+
await this.sleep(delay);
|
|
1222
|
+
}
|
|
1206
1223
|
}
|
|
1207
1224
|
}
|
|
1208
1225
|
return results;
|
|
@@ -1591,24 +1608,36 @@ var TokenBucketRateLimiter = class {
|
|
|
1591
1608
|
const startTime = Date.now();
|
|
1592
1609
|
if (this.processing) {
|
|
1593
1610
|
const acquired = await new Promise((resolve) => {
|
|
1611
|
+
let timeoutInterval = null;
|
|
1612
|
+
let resolved = false;
|
|
1613
|
+
const cleanup = () => {
|
|
1614
|
+
if (timeoutInterval !== null) {
|
|
1615
|
+
clearInterval(timeoutInterval);
|
|
1616
|
+
timeoutInterval = null;
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1594
1619
|
const checkTimeout = () => {
|
|
1595
1620
|
if (Date.now() - startTime > timeoutMs) {
|
|
1596
1621
|
const idx = this.requestQueue.indexOf(resolveCallback);
|
|
1597
1622
|
if (idx >= 0) {
|
|
1598
1623
|
this.requestQueue.splice(idx, 1);
|
|
1599
1624
|
}
|
|
1600
|
-
|
|
1625
|
+
if (!resolved) {
|
|
1626
|
+
resolved = true;
|
|
1627
|
+
cleanup();
|
|
1628
|
+
resolve(false);
|
|
1629
|
+
}
|
|
1601
1630
|
}
|
|
1602
1631
|
};
|
|
1603
|
-
const resolveCallback = () =>
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
const cleanup = () => clearInterval(timeoutInterval);
|
|
1607
|
-
Promise.resolve().then(() => {
|
|
1608
|
-
if (this.requestQueue.includes(resolveCallback)) ; else {
|
|
1632
|
+
const resolveCallback = () => {
|
|
1633
|
+
if (!resolved) {
|
|
1634
|
+
resolved = true;
|
|
1609
1635
|
cleanup();
|
|
1636
|
+
resolve(true);
|
|
1610
1637
|
}
|
|
1611
|
-
}
|
|
1638
|
+
};
|
|
1639
|
+
this.requestQueue.push(resolveCallback);
|
|
1640
|
+
timeoutInterval = setInterval(checkTimeout, 1e3);
|
|
1612
1641
|
});
|
|
1613
1642
|
if (!acquired) {
|
|
1614
1643
|
return false;
|
|
@@ -1616,24 +1645,25 @@ var TokenBucketRateLimiter = class {
|
|
|
1616
1645
|
}
|
|
1617
1646
|
this.processing = true;
|
|
1618
1647
|
try {
|
|
1619
|
-
this.refill();
|
|
1620
1648
|
if (this.currentBackoff > 0) {
|
|
1621
1649
|
this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
|
|
1622
1650
|
await sleep(this.currentBackoff);
|
|
1623
1651
|
this.lastRefill = Date.now();
|
|
1624
|
-
this.currentBackoff = 0;
|
|
1625
1652
|
}
|
|
1653
|
+
this.refill();
|
|
1626
1654
|
while (this.tokens <= 0) {
|
|
1627
1655
|
if (Date.now() - startTime > timeoutMs) {
|
|
1628
1656
|
return false;
|
|
1629
1657
|
}
|
|
1630
|
-
|
|
1658
|
+
const waitTime = Math.min(100, this.config.minDelayMs);
|
|
1659
|
+
await sleep(waitTime);
|
|
1631
1660
|
this.refill();
|
|
1632
1661
|
}
|
|
1633
1662
|
this.tokens--;
|
|
1634
1663
|
const timeSinceLastRefill = Date.now() - this.lastRefill;
|
|
1635
1664
|
if (timeSinceLastRefill < this.config.minDelayMs) {
|
|
1636
|
-
|
|
1665
|
+
const remainingDelay = this.config.minDelayMs - timeSinceLastRefill;
|
|
1666
|
+
await sleep(remainingDelay);
|
|
1637
1667
|
}
|
|
1638
1668
|
this.lastRefill = Date.now();
|
|
1639
1669
|
return true;
|
|
@@ -1651,10 +1681,21 @@ var TokenBucketRateLimiter = class {
|
|
|
1651
1681
|
release() {
|
|
1652
1682
|
}
|
|
1653
1683
|
/**
|
|
1654
|
-
* Report a successful request (
|
|
1684
|
+
* Report a successful request (gradually reduces backoff)
|
|
1685
|
+
*
|
|
1686
|
+
* Instead of immediately clearing backoff, we gradually reduce it to prevent
|
|
1687
|
+
* immediately hitting the rate limit again after a single success.
|
|
1655
1688
|
*/
|
|
1656
1689
|
reportSuccess() {
|
|
1657
|
-
this.currentBackoff
|
|
1690
|
+
if (this.currentBackoff > 0) {
|
|
1691
|
+
this.currentBackoff = Math.max(
|
|
1692
|
+
this.config.minDelayMs,
|
|
1693
|
+
Math.floor(this.currentBackoff / 2)
|
|
1694
|
+
);
|
|
1695
|
+
if (this.currentBackoff <= this.config.minDelayMs) {
|
|
1696
|
+
this.currentBackoff = 0;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1658
1699
|
this.consecutiveErrors = 0;
|
|
1659
1700
|
}
|
|
1660
1701
|
/**
|
|
@@ -1845,7 +1886,9 @@ var DEFAULT_CONFIG2 = {
|
|
|
1845
1886
|
latencyWeight: 0.4,
|
|
1846
1887
|
priorityWeight: 0.3,
|
|
1847
1888
|
freshnessWeight: 0.3,
|
|
1848
|
-
minStatus: ["available", "degraded"]
|
|
1889
|
+
minStatus: ["available", "degraded"],
|
|
1890
|
+
retryCooldownMs: 3e4
|
|
1891
|
+
// 30 seconds
|
|
1849
1892
|
};
|
|
1850
1893
|
var ProviderSelector = class {
|
|
1851
1894
|
constructor(registry, healthChecker, config, logger, adapter = "node") {
|
|
@@ -1939,14 +1982,19 @@ var ProviderSelector = class {
|
|
|
1939
1982
|
return defaultProvider;
|
|
1940
1983
|
}
|
|
1941
1984
|
if (health.success === false && health.lastTested) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1985
|
+
if (this.isRetryableError(health.error)) {
|
|
1986
|
+
const timeSinceFailure = Date.now() - health.lastTested.getTime();
|
|
1987
|
+
if (timeSinceFailure > this.config.retryCooldownMs) {
|
|
1988
|
+
this.logger.warn(
|
|
1989
|
+
`No healthy providers for ${network}, retrying failed default after cooldown: ${defaultProvider.id}`
|
|
1990
|
+
);
|
|
1991
|
+
this.activeProviderByNetwork.set(network, defaultProvider.id);
|
|
1992
|
+
return defaultProvider;
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
this.logger.debug(
|
|
1996
|
+
`Skipping provider ${defaultProvider.id} with permanent error: ${health.error}`
|
|
1947
1997
|
);
|
|
1948
|
-
this.activeProviderByNetwork.set(network, defaultProvider.id);
|
|
1949
|
-
return defaultProvider;
|
|
1950
1998
|
}
|
|
1951
1999
|
}
|
|
1952
2000
|
}
|
|
@@ -1960,14 +2008,19 @@ var ProviderSelector = class {
|
|
|
1960
2008
|
return provider;
|
|
1961
2009
|
}
|
|
1962
2010
|
if (health.success === false && health.lastTested) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2011
|
+
if (this.isRetryableError(health.error)) {
|
|
2012
|
+
const timeSinceFailure = Date.now() - health.lastTested.getTime();
|
|
2013
|
+
if (timeSinceFailure > this.config.retryCooldownMs) {
|
|
2014
|
+
this.logger.warn(
|
|
2015
|
+
`No healthy providers for ${network}, retrying failed provider after cooldown: ${provider.id}`
|
|
2016
|
+
);
|
|
2017
|
+
this.activeProviderByNetwork.set(network, provider.id);
|
|
2018
|
+
return provider;
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
this.logger.debug(
|
|
2022
|
+
`Skipping provider ${provider.id} with permanent error: ${health.error}`
|
|
1968
2023
|
);
|
|
1969
|
-
this.activeProviderByNetwork.set(network, provider.id);
|
|
1970
|
-
return provider;
|
|
1971
2024
|
}
|
|
1972
2025
|
}
|
|
1973
2026
|
}
|
|
@@ -1982,6 +2035,12 @@ var ProviderSelector = class {
|
|
|
1982
2035
|
this.logger.debug(
|
|
1983
2036
|
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
|
|
1984
2037
|
);
|
|
2038
|
+
} else if (bestHealth && bestHealth.success === false) {
|
|
2039
|
+
this.bestProviderByNetwork.delete(network);
|
|
2040
|
+
this.activeProviderByNetwork.set(network, best.id);
|
|
2041
|
+
this.logger.debug(
|
|
2042
|
+
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)}, failed - not cached)`
|
|
2043
|
+
);
|
|
1985
2044
|
} else {
|
|
1986
2045
|
this.activeProviderByNetwork.set(network, best.id);
|
|
1987
2046
|
this.logger.debug(
|
|
@@ -2032,11 +2091,12 @@ var ProviderSelector = class {
|
|
|
2032
2091
|
return 0.01 * (1 / (provider.priority + 1));
|
|
2033
2092
|
}
|
|
2034
2093
|
if (health.success === false) {
|
|
2035
|
-
if (health.
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2094
|
+
if (this.isRetryableError(health.error)) {
|
|
2095
|
+
if (health.lastTested) {
|
|
2096
|
+
const timeSinceFailure = Date.now() - health.lastTested.getTime();
|
|
2097
|
+
if (timeSinceFailure > this.config.retryCooldownMs) {
|
|
2098
|
+
return 1e-3 * (1 / (provider.priority + 1));
|
|
2099
|
+
}
|
|
2040
2100
|
}
|
|
2041
2101
|
}
|
|
2042
2102
|
return 0;
|
|
@@ -2231,6 +2291,34 @@ var ProviderSelector = class {
|
|
|
2231
2291
|
return true;
|
|
2232
2292
|
});
|
|
2233
2293
|
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Check if an error is retryable (temporary) or permanent
|
|
2296
|
+
*
|
|
2297
|
+
* Permanent errors (never retry):
|
|
2298
|
+
* - 404 (Not Found) - endpoint doesn't exist
|
|
2299
|
+
* - 401 (Unauthorized) - invalid API key
|
|
2300
|
+
* - Invalid API key errors
|
|
2301
|
+
*
|
|
2302
|
+
* Retryable errors (can retry after cooldown):
|
|
2303
|
+
* - 503 (Service Unavailable) - temporary server issue
|
|
2304
|
+
* - 502 (Bad Gateway) - temporary gateway issue
|
|
2305
|
+
* - Timeout errors - network issues
|
|
2306
|
+
* - Network errors - connection issues
|
|
2307
|
+
* - 429 (Rate Limit) - temporary rate limit (handled separately)
|
|
2308
|
+
*/
|
|
2309
|
+
isRetryableError(error) {
|
|
2310
|
+
if (!error) {
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
const errorLower = error.toLowerCase();
|
|
2314
|
+
if (errorLower.includes("404") || errorLower.includes("not found") || errorLower.includes("401") || errorLower.includes("unauthorized") || errorLower.includes("invalid api key") || errorLower.includes("api key is invalid") || errorLower.includes("authentication failed") || errorLower.includes("forbidden")) {
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
if (errorLower.includes("503") || errorLower.includes("service unavailable") || errorLower.includes("502") || errorLower.includes("bad gateway") || errorLower.includes("timeout") || errorLower.includes("network error") || errorLower.includes("connection") || errorLower.includes("econnrefused") || errorLower.includes("enotfound")) {
|
|
2318
|
+
return true;
|
|
2319
|
+
}
|
|
2320
|
+
return true;
|
|
2321
|
+
}
|
|
2234
2322
|
};
|
|
2235
2323
|
function createSelector(registry, healthChecker, config, logger, adapter = "node") {
|
|
2236
2324
|
return new ProviderSelector(registry, healthChecker, config, logger, adapter);
|
|
@@ -2654,10 +2742,17 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2654
2742
|
}
|
|
2655
2743
|
const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
|
|
2656
2744
|
if (!acquired) {
|
|
2657
|
-
|
|
2745
|
+
const timeoutError = new Error(`Rate limit token acquisition timeout for ${provider.id} after ${timeoutMs || 6e4}ms`);
|
|
2746
|
+
this.options.logger.warn(timeoutError.message);
|
|
2747
|
+
this.reportError(timeoutError);
|
|
2658
2748
|
const next = this.selector.getNextProvider(this.network, [provider.id]);
|
|
2659
2749
|
if (next) {
|
|
2660
|
-
|
|
2750
|
+
const nextTimeout = timeoutMs ? Math.min(timeoutMs, 1e4) : 1e4;
|
|
2751
|
+
const nextAcquired = await this.rateLimiter.acquire(next.id, nextTimeout);
|
|
2752
|
+
if (nextAcquired) {
|
|
2753
|
+
return normalizeV2Endpoint(next.endpointV2, next);
|
|
2754
|
+
}
|
|
2755
|
+
this.options.logger.warn(`Rate limit timeout for next provider ${next.id}, using fallback`);
|
|
2661
2756
|
}
|
|
2662
2757
|
return this.getFallbackEndpoint();
|
|
2663
2758
|
}
|
|
@@ -2707,26 +2802,48 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2707
2802
|
if (!provider) {
|
|
2708
2803
|
provider = this.selector.getBestProvider(this.network);
|
|
2709
2804
|
}
|
|
2710
|
-
if (!provider)
|
|
2805
|
+
if (!provider) {
|
|
2806
|
+
this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2711
2809
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2712
2810
|
const errorMsgLower = errorMsg.toLowerCase();
|
|
2713
|
-
const
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
2811
|
+
const enhancedErrorMsg = `Provider ${provider.id} (${provider.name}): ${errorMsg}`;
|
|
2812
|
+
const responseStatus = error instanceof Error && error?.response?.status || error instanceof Error && error?.status || error instanceof Error && error?.statusCode || null;
|
|
2813
|
+
const statusMatch = errorMsg.match(/\b(\d{3})\b/);
|
|
2814
|
+
const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
2815
|
+
const httpStatus = responseStatus || statusFromMsg;
|
|
2816
|
+
const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
|
|
2817
|
+
const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
|
|
2818
|
+
const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
|
|
2819
|
+
const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
|
|
2820
|
+
const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
|
|
2821
|
+
const isTimeout = error instanceof Error && error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("abort");
|
|
2718
2822
|
if (isRateLimitError(error) || is429) {
|
|
2719
2823
|
this.rateLimiter.reportRateLimitError(provider.id);
|
|
2720
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
2721
|
-
|
|
2824
|
+
this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
|
|
2825
|
+
this.options.logger.warn(`${enhancedErrorMsg} - Rate limit detected, switching to next provider`);
|
|
2826
|
+
} else if (is404 || is401) {
|
|
2827
|
+
this.rateLimiter.reportError(provider.id);
|
|
2828
|
+
this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
|
|
2829
|
+
this.options.logger.error(`${enhancedErrorMsg} - Permanent error (${is404 ? "404" : "401"}), provider marked offline`);
|
|
2830
|
+
} else if (is503 || is502 || isTimeout) {
|
|
2722
2831
|
this.rateLimiter.reportError(provider.id);
|
|
2723
|
-
this.healthChecker.markOffline(provider.id, this.network,
|
|
2832
|
+
this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
|
|
2833
|
+
const errorType = is503 ? "503" : is502 ? "502" : "timeout";
|
|
2834
|
+
this.options.logger.warn(`${enhancedErrorMsg} - Server error (${errorType}), switching to next provider`);
|
|
2724
2835
|
} else {
|
|
2725
2836
|
this.rateLimiter.reportError(provider.id);
|
|
2726
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
2837
|
+
this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
|
|
2838
|
+
this.options.logger.warn(`${enhancedErrorMsg} - Unknown error, switching to next provider`);
|
|
2839
|
+
}
|
|
2840
|
+
const nextProvider = this.selector.handleProviderFailure(provider.id, this.network);
|
|
2841
|
+
if (nextProvider) {
|
|
2842
|
+
this.options.logger.info(`Failover: switched from ${provider.id} to ${nextProvider.id}`);
|
|
2843
|
+
} else {
|
|
2844
|
+
this.options.logger.warn(`Failover: no alternative provider available for ${this.network}`);
|
|
2727
2845
|
}
|
|
2728
2846
|
this.selector.clearCache(this.network);
|
|
2729
|
-
this.selector.handleProviderFailure(provider.id, this.network);
|
|
2730
2847
|
this.notifyListeners();
|
|
2731
2848
|
}
|
|
2732
2849
|
// ========================================================================
|
|
@@ -3277,9 +3394,11 @@ var BrowserAdapter = class {
|
|
|
3277
3394
|
// ========================================================================
|
|
3278
3395
|
/**
|
|
3279
3396
|
* Make a JSON-RPC call to the TON API
|
|
3397
|
+
*
|
|
3398
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3280
3399
|
*/
|
|
3281
3400
|
async jsonRpc(method, params = {}, timeoutMs = 1e4) {
|
|
3282
|
-
const endpoint = await this.manager.
|
|
3401
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3283
3402
|
const controller = new AbortController();
|
|
3284
3403
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3285
3404
|
try {
|
|
@@ -3318,9 +3437,11 @@ var BrowserAdapter = class {
|
|
|
3318
3437
|
// ========================================================================
|
|
3319
3438
|
/**
|
|
3320
3439
|
* Get address state
|
|
3440
|
+
*
|
|
3441
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3321
3442
|
*/
|
|
3322
3443
|
async getAddressState(address, timeoutMs = 1e4) {
|
|
3323
|
-
const endpoint = await this.manager.
|
|
3444
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3324
3445
|
const baseV2 = toV2Base(endpoint);
|
|
3325
3446
|
const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
3326
3447
|
const controller = new AbortController();
|
|
@@ -3350,9 +3471,11 @@ var BrowserAdapter = class {
|
|
|
3350
3471
|
}
|
|
3351
3472
|
/**
|
|
3352
3473
|
* Get address balance
|
|
3474
|
+
*
|
|
3475
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3353
3476
|
*/
|
|
3354
3477
|
async getAddressBalance(address, timeoutMs = 1e4) {
|
|
3355
|
-
const endpoint = await this.manager.
|
|
3478
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3356
3479
|
const baseV2 = toV2Base(endpoint);
|
|
3357
3480
|
const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
3358
3481
|
const controller = new AbortController();
|
|
@@ -3382,9 +3505,11 @@ var BrowserAdapter = class {
|
|
|
3382
3505
|
}
|
|
3383
3506
|
/**
|
|
3384
3507
|
* Get address information
|
|
3508
|
+
*
|
|
3509
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3385
3510
|
*/
|
|
3386
3511
|
async getAddressInfo(address, timeoutMs = 1e4) {
|
|
3387
|
-
const endpoint = await this.manager.
|
|
3512
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3388
3513
|
const baseV2 = toV2Base(endpoint);
|
|
3389
3514
|
const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
3390
3515
|
const controller = new AbortController();
|
|
@@ -3412,9 +3537,11 @@ var BrowserAdapter = class {
|
|
|
3412
3537
|
}
|
|
3413
3538
|
/**
|
|
3414
3539
|
* Run get method
|
|
3540
|
+
*
|
|
3541
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3415
3542
|
*/
|
|
3416
3543
|
async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
|
|
3417
|
-
const endpoint = await this.manager.
|
|
3544
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3418
3545
|
const baseV2 = toV2Base(endpoint);
|
|
3419
3546
|
const url = `${baseV2}/runGetMethod`;
|
|
3420
3547
|
const controller = new AbortController();
|