@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/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
- const response = await fetch(url.toString(), {
2426
- method,
2427
- headers: this.headers,
2428
- signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
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}`, {