@adaptic/utils 0.0.1005 → 0.0.1007
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 +1992 -1935
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1992 -1935
- package/dist/index.mjs.map +1 -1
- package/dist/test.js +155 -5
- package/dist/test.js.map +1 -1
- package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/test.js
CHANGED
|
@@ -1537,6 +1537,99 @@ function createTimeoutSignal(ms) {
|
|
|
1537
1537
|
return AbortSignal.timeout(ms);
|
|
1538
1538
|
}
|
|
1539
1539
|
|
|
1540
|
+
/**
|
|
1541
|
+
* Node.js / undici / system error codes that represent transient network
|
|
1542
|
+
* conditions. Present on `error.code` for net/http/dns/undici errors.
|
|
1543
|
+
*/
|
|
1544
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
1545
|
+
"ETIMEDOUT",
|
|
1546
|
+
"ESOCKETTIMEDOUT",
|
|
1547
|
+
"ECONNRESET",
|
|
1548
|
+
"ECONNREFUSED",
|
|
1549
|
+
"EHOSTUNREACH",
|
|
1550
|
+
"ENETUNREACH",
|
|
1551
|
+
"EAI_AGAIN",
|
|
1552
|
+
"EPIPE",
|
|
1553
|
+
"ECONNABORTED",
|
|
1554
|
+
"ENOTFOUND",
|
|
1555
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
1556
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
1557
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
1558
|
+
"UND_ERR_SOCKET",
|
|
1559
|
+
"UND_ERR_CLOSED",
|
|
1560
|
+
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
|
|
1561
|
+
]);
|
|
1562
|
+
/**
|
|
1563
|
+
* Error constructor names / `error.name` values that indicate transient
|
|
1564
|
+
* abort / timeout conditions.
|
|
1565
|
+
*/
|
|
1566
|
+
const RETRYABLE_ERROR_NAMES = new Set([
|
|
1567
|
+
"AbortError",
|
|
1568
|
+
"TimeoutError",
|
|
1569
|
+
"FetchError",
|
|
1570
|
+
"RequestTimeoutError",
|
|
1571
|
+
"ConnectTimeoutError",
|
|
1572
|
+
"HeadersTimeoutError",
|
|
1573
|
+
"BodyTimeoutError",
|
|
1574
|
+
]);
|
|
1575
|
+
/**
|
|
1576
|
+
* Message-pattern fallback for libraries that discard error codes/names but
|
|
1577
|
+
* preserve text (e.g., some Apollo/axios wrappers).
|
|
1578
|
+
*/
|
|
1579
|
+
const RETRYABLE_MESSAGE_PATTERNS = [
|
|
1580
|
+
/aborted/i,
|
|
1581
|
+
/timeout/i,
|
|
1582
|
+
/timed out/i,
|
|
1583
|
+
/network error/i,
|
|
1584
|
+
/socket hang up/i,
|
|
1585
|
+
/connection (reset|refused|closed)/i,
|
|
1586
|
+
/ECONNRESET/,
|
|
1587
|
+
/ETIMEDOUT/,
|
|
1588
|
+
/ECONNREFUSED/,
|
|
1589
|
+
/EAI_AGAIN/,
|
|
1590
|
+
/UND_ERR_/,
|
|
1591
|
+
];
|
|
1592
|
+
/**
|
|
1593
|
+
* Walks the `error.cause` chain (capped to avoid cycles) and tests whether
|
|
1594
|
+
* any link along the chain looks like a transient network error. Modern APIs
|
|
1595
|
+
* (undici, fetch, Apollo Client 3.8+) wrap the root network failure as a
|
|
1596
|
+
* `.cause`, so the surface `Error` may report a generic message while the
|
|
1597
|
+
* actionable signal lives one or more levels deeper.
|
|
1598
|
+
*
|
|
1599
|
+
* Exported for use by downstream consumers (engine services, per-call catch
|
|
1600
|
+
* blocks, application-level loggers) that need to demote recoverable
|
|
1601
|
+
* transient errors from ERROR to WARN. Aligns the whole stack on a single
|
|
1602
|
+
* canonical classifier so MassiveAPI, AlpacaAPI, and application-layer
|
|
1603
|
+
* retry handlers all treat the same network blips identically.
|
|
1604
|
+
*/
|
|
1605
|
+
function isTransientNetworkError(error) {
|
|
1606
|
+
const MAX_CAUSE_DEPTH = 6;
|
|
1607
|
+
let current = error;
|
|
1608
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
1609
|
+
if (current instanceof Error || typeof current === "object") {
|
|
1610
|
+
const err = current;
|
|
1611
|
+
if (typeof err.name === "string" && RETRYABLE_ERROR_NAMES.has(err.name)) {
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
if (typeof err.code === "string" && RETRYABLE_ERROR_CODES.has(err.code)) {
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof err.message === "string") {
|
|
1618
|
+
for (const pattern of RETRYABLE_MESSAGE_PATTERNS) {
|
|
1619
|
+
if (pattern.test(err.message)) {
|
|
1620
|
+
return true;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
current = err.cause;
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1540
1633
|
/**
|
|
1541
1634
|
* Structured error type hierarchy for all API integrations
|
|
1542
1635
|
*
|
|
@@ -1912,6 +2005,32 @@ const rateLimiters = {
|
|
|
1912
2005
|
}),
|
|
1913
2006
|
};
|
|
1914
2007
|
|
|
2008
|
+
/**
|
|
2009
|
+
* Bounded retry for TRANSIENT network faults on Alpaca market-data reads.
|
|
2010
|
+
*
|
|
2011
|
+
* 2026-07-30: the engine logged 637 `TypeError: fetch failed` / `read
|
|
2012
|
+
* ECONNRESET` errors against `/v2/stocks/bars` in a single session (~4 per
|
|
2013
|
+
* minute). Every OTHER Alpaca/Massive client in this package already routes
|
|
2014
|
+
* transient faults through {@link isTransientNetworkError} — this client was
|
|
2015
|
+
* the one that never adopted it, so a single reset socket killed the whole
|
|
2016
|
+
* read. Those failures surface in the engine's market-tape prompt context and
|
|
2017
|
+
* its intraday direction/path resolvers, so the decision model judged with a
|
|
2018
|
+
* blind tape: raw confidence collapsed from a 0.677 median to a degenerate
|
|
2019
|
+
* ~0.504 band, the entire distribution fell below the 0.55 admission floor,
|
|
2020
|
+
* and live participation went to near zero.
|
|
2021
|
+
*
|
|
2022
|
+
* `ECONNRESET` on a keep-alive pool is the classic idle-socket race — the
|
|
2023
|
+
* server closes a pooled connection while the client dispatches onto it. It
|
|
2024
|
+
* is transient by construction and safe to retry on an idempotent GET.
|
|
2025
|
+
*/
|
|
2026
|
+
const TRANSIENT_NETWORK_RETRY_ATTEMPTS = 3;
|
|
2027
|
+
/** Base backoff (ms); doubled per attempt with full jitter. */
|
|
2028
|
+
const TRANSIENT_NETWORK_RETRY_BASE_MS = 120;
|
|
2029
|
+
/** Full-jitter backoff so concurrent fan-out retries do not resonate. */
|
|
2030
|
+
function transientRetryDelayMs(attempt) {
|
|
2031
|
+
const ceiling = TRANSIENT_NETWORK_RETRY_BASE_MS * 2 ** attempt;
|
|
2032
|
+
return Math.floor(Math.random() * ceiling);
|
|
2033
|
+
}
|
|
1915
2034
|
const log$1 = (message, options = { type: "info" }) => {
|
|
1916
2035
|
log$2(message, { ...options, source: "AlpacaMarketDataAPI" });
|
|
1917
2036
|
};
|
|
@@ -2422,11 +2541,42 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
2422
2541
|
// can't overrun Alpaca's server-side rate limit. Prior to this, parallel
|
|
2423
2542
|
// historical-bar fan-out produced ~125 server-side 429s per minute.
|
|
2424
2543
|
await rateLimiters.alpaca.acquire();
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2544
|
+
// Retry ONLY transient connection faults, and only on GET (every
|
|
2545
|
+
// market-data read here is idempotent). A non-2xx response is a real
|
|
2546
|
+
// answer from Alpaca and is never retried — that path still throws on
|
|
2547
|
+
// the first attempt exactly as before.
|
|
2548
|
+
let response;
|
|
2549
|
+
let lastNetworkError;
|
|
2550
|
+
for (let attempt = 0; attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS; attempt += 1) {
|
|
2551
|
+
try {
|
|
2552
|
+
response = await fetch(url.toString(), {
|
|
2553
|
+
method,
|
|
2554
|
+
headers: this.headers,
|
|
2555
|
+
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
|
|
2556
|
+
});
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
catch (networkErr) {
|
|
2560
|
+
lastNetworkError = networkErr;
|
|
2561
|
+
const retryable = method === "GET" &&
|
|
2562
|
+
isTransientNetworkError(networkErr) &&
|
|
2563
|
+
attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1;
|
|
2564
|
+
if (!retryable) {
|
|
2565
|
+
throw networkErr;
|
|
2566
|
+
}
|
|
2567
|
+
const delayMs = transientRetryDelayMs(attempt);
|
|
2568
|
+
log$1(`Transient network fault on ${endpoint} (attempt ${attempt + 1}/${TRANSIENT_NETWORK_RETRY_ATTEMPTS}); retrying in ${delayMs}ms`, { type: "warn" });
|
|
2569
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2570
|
+
// Re-acquire the rate-limit token so a retry storm cannot overrun
|
|
2571
|
+
// Alpaca's server-side limit.
|
|
2572
|
+
await rateLimiters.alpaca.acquire();
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (!response) {
|
|
2576
|
+
throw lastNetworkError instanceof Error
|
|
2577
|
+
? lastNetworkError
|
|
2578
|
+
: new Error(`Market Data API request failed for ${endpoint}`);
|
|
2579
|
+
}
|
|
2430
2580
|
if (!response.ok) {
|
|
2431
2581
|
const errorText = await response.text();
|
|
2432
2582
|
log$1(`Market Data API error (${response.status}): ${errorText}`, {
|