@adaptic/utils 0.0.1006 → 0.0.1008

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.
Files changed (31) hide show
  1. package/dist/index.cjs +586 -177
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.mjs +587 -179
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/test.js +120 -9
  6. package/dist/test.js.map +1 -1
  7. package/dist/types/__tests__/alpaca-client-order-id.test.d.ts +2 -0
  8. package/dist/types/__tests__/alpaca-client-order-id.test.d.ts.map +1 -0
  9. package/dist/types/__tests__/alpaca-market-data-retry.test.d.ts +2 -0
  10. package/dist/types/__tests__/alpaca-market-data-retry.test.d.ts.map +1 -0
  11. package/dist/types/__tests__/performance-metrics-fees.test.d.ts +2 -0
  12. package/dist/types/__tests__/performance-metrics-fees.test.d.ts.map +1 -0
  13. package/dist/types/__tests__/price-utils-fees.test.d.ts +2 -0
  14. package/dist/types/__tests__/price-utils-fees.test.d.ts.map +1 -0
  15. package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
  16. package/dist/types/alpaca-trading-api.d.ts +93 -8
  17. package/dist/types/alpaca-trading-api.d.ts.map +1 -1
  18. package/dist/types/cache/stampede-protected-cache.d.ts +19 -1
  19. package/dist/types/cache/stampede-protected-cache.d.ts.map +1 -1
  20. package/dist/types/errors/index.d.ts +25 -0
  21. package/dist/types/errors/index.d.ts.map +1 -1
  22. package/dist/types/index.d.ts +5 -5
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/types/performance-metrics.d.ts +31 -1
  25. package/dist/types/performance-metrics.d.ts.map +1 -1
  26. package/dist/types/price-utils.d.ts +12 -0
  27. package/dist/types/price-utils.d.ts.map +1 -1
  28. package/dist/types/rate-limiter.d.ts.map +1 -1
  29. package/dist/types/utils/retry.d.ts +14 -0
  30. package/dist/types/utils/retry.d.ts.map +1 -1
  31. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -2276,6 +2276,80 @@ function isTransientNetworkError(error) {
2276
2276
  }
2277
2277
  return false;
2278
2278
  }
2279
+ /**
2280
+ * Error names that indicate the CLIENT's own request deadline expired (an
2281
+ * `AbortSignal` timeout or an undici per-phase timeout) rather than a
2282
+ * connection-phase fault. These faults have already consumed a full request
2283
+ * timeout, so retrying them is expensive by construction.
2284
+ */
2285
+ const DEADLINE_EXPIRY_ERROR_NAMES = new Set([
2286
+ "AbortError",
2287
+ "TimeoutError",
2288
+ "RequestTimeoutError",
2289
+ "ConnectTimeoutError",
2290
+ "HeadersTimeoutError",
2291
+ "BodyTimeoutError",
2292
+ ]);
2293
+ /**
2294
+ * Error codes that indicate an expired request/phase deadline (vs a fast
2295
+ * connection-phase fault such as `ECONNRESET`/`ECONNREFUSED`).
2296
+ */
2297
+ const DEADLINE_EXPIRY_ERROR_CODES = new Set([
2298
+ "ETIMEDOUT",
2299
+ "ESOCKETTIMEDOUT",
2300
+ "ECONNABORTED",
2301
+ "UND_ERR_CONNECT_TIMEOUT",
2302
+ "UND_ERR_HEADERS_TIMEOUT",
2303
+ "UND_ERR_BODY_TIMEOUT",
2304
+ ]);
2305
+ /** Message-pattern fallback for deadline-expiry errors that lost name/code. */
2306
+ const DEADLINE_EXPIRY_MESSAGE_PATTERNS = [
2307
+ /timed out/i,
2308
+ /timeout/i,
2309
+ /aborted/i,
2310
+ ];
2311
+ /**
2312
+ * Whether an error represents the client's OWN deadline expiring (abort /
2313
+ * timeout) rather than a connection-phase network fault. Both classes are
2314
+ * "transient" per {@link isTransientNetworkError}, but they have very
2315
+ * different retry economics: a connection fault (`ECONNRESET`, `EPIPE`,
2316
+ * refused socket) settles in milliseconds and is cheap to retry, while a
2317
+ * deadline expiry has already consumed the full per-attempt timeout — blindly
2318
+ * retrying it multiplies time-to-failure exactly when the caller most needs
2319
+ * to fail fast. Walks the `error.cause` chain like the transient classifier.
2320
+ *
2321
+ * @param error - The error to classify.
2322
+ * @returns true when the fault is a client deadline/abort expiry.
2323
+ */
2324
+ function isClientDeadlineExpiry(error) {
2325
+ const MAX_CAUSE_DEPTH = 6;
2326
+ let current = error;
2327
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
2328
+ if (current instanceof Error || typeof current === "object") {
2329
+ const err = current;
2330
+ if (typeof err.name === "string" &&
2331
+ DEADLINE_EXPIRY_ERROR_NAMES.has(err.name)) {
2332
+ return true;
2333
+ }
2334
+ if (typeof err.code === "string" &&
2335
+ DEADLINE_EXPIRY_ERROR_CODES.has(err.code)) {
2336
+ return true;
2337
+ }
2338
+ if (typeof err.message === "string") {
2339
+ for (const pattern of DEADLINE_EXPIRY_MESSAGE_PATTERNS) {
2340
+ if (pattern.test(err.message)) {
2341
+ return true;
2342
+ }
2343
+ }
2344
+ }
2345
+ current = err.cause;
2346
+ }
2347
+ else {
2348
+ break;
2349
+ }
2350
+ }
2351
+ return false;
2352
+ }
2279
2353
  /**
2280
2354
  * Analyzes an error and determines if it's retryable.
2281
2355
  * @param error - The error to analyze
@@ -2803,6 +2877,33 @@ class DataFormatError extends AdapticUtilsError {
2803
2877
  this.service = service;
2804
2878
  }
2805
2879
  }
2880
+ /**
2881
+ * Broker-side duplicate `client_order_id` rejection (Alpaca HTTP 422,
2882
+ * "client order id must be unique").
2883
+ *
2884
+ * Thrown by the order-creation paths of `AlpacaTradingAPI` so callers can
2885
+ * distinguish "this exact order was already submitted" from a genuine order
2886
+ * rejection. When {@link wasDerived} is `false` the id was caller-supplied and
2887
+ * the caller owns idempotency semantics (a legitimate repeat needs a new
2888
+ * explicit id or an `idempotencyNonce`). When `true`, the wrapper's automatic
2889
+ * recovery (existing-order lookup, then one salted resubmit) was exhausted.
2890
+ *
2891
+ * Never retryable with the same id — resubmitting the identical
2892
+ * `client_order_id` will 422 again.
2893
+ */
2894
+ class DuplicateClientOrderIdError extends AlpacaApiError {
2895
+ clientOrderId;
2896
+ wasDerived;
2897
+ constructor(message,
2898
+ /** The `client_order_id` that collided broker-side. */
2899
+ clientOrderId,
2900
+ /** Whether the colliding id was derived by the wrapper (vs caller-supplied). */
2901
+ wasDerived, cause) {
2902
+ super(message, "DUPLICATE_CLIENT_ORDER_ID", 422, cause);
2903
+ this.clientOrderId = clientOrderId;
2904
+ this.wasDerived = wasDerived;
2905
+ }
2906
+ }
2806
2907
 
2807
2908
  /**
2808
2909
  * Token bucket rate limiter for external API integrations
@@ -2900,8 +3001,11 @@ class TokenBucketRateLimiter {
2900
3001
  async acquire() {
2901
3002
  const logger = getLogger();
2902
3003
  this.refill();
2903
- if (this.tokens > 0) {
2904
- this.tokens--;
3004
+ // Require a WHOLE token: refill() accrues fractionally, and admitting on
3005
+ // any positive fraction would release a full request per accrual tick,
3006
+ // driving the bucket negative and overrunning the configured rate.
3007
+ if (this.tokens >= TOKENS_PER_REQUEST) {
3008
+ this.tokens -= TOKENS_PER_REQUEST;
2905
3009
  logger.debug(`Rate limit token acquired for ${this.config.label}`, {
2906
3010
  remainingTokens: this.tokens,
2907
3011
  queueLength: this.queue.length,
@@ -2997,8 +3101,9 @@ class TokenBucketRateLimiter {
2997
3101
  this.processingQueue = true;
2998
3102
  const logger = getLogger();
2999
3103
  try {
3000
- while (this.queue.length > 0 && this.tokens > 0) {
3001
- this.tokens--;
3104
+ // Whole-token admission — see the matching guard in acquire().
3105
+ while (this.queue.length > 0 && this.tokens >= TOKENS_PER_REQUEST) {
3106
+ this.tokens -= TOKENS_PER_REQUEST;
3002
3107
  const next = this.queue.shift();
3003
3108
  if (next) {
3004
3109
  clearTimeout(next.timeoutHandle);
@@ -3160,6 +3265,23 @@ function transientRetryDelayMs(attempt) {
3160
3265
  const ceiling = TRANSIENT_NETWORK_RETRY_BASE_MS * 2 ** attempt;
3161
3266
  return Math.floor(Math.random() * ceiling);
3162
3267
  }
3268
+ /**
3269
+ * Total-deadline multiple over the per-attempt client timeout for one
3270
+ * {@link AlpacaMarketDataAPI.makeRequest} call INCLUDING transient retries.
3271
+ * The ECONNRESET class this loop targets settles in milliseconds, so the
3272
+ * retries fit comfortably inside 1.5x the single-attempt timeout — while a
3273
+ * request whose attempts each consume the full client timeout is refused
3274
+ * further retries instead of stretching a hot-path read to a multi-minute
3275
+ * stall (worst case before this budget: 3 x (60s limiter wait + 30s fetch)).
3276
+ */
3277
+ const TRANSIENT_RETRY_BUDGET_TIMEOUT_MULTIPLE = 1.5;
3278
+ /**
3279
+ * Maximum retries for a fault classified as the client's OWN deadline expiry
3280
+ * (see {@link isClientDeadlineExpiry}): such a fault already consumed a full
3281
+ * per-attempt timeout, so it is retried at most once — and then only if the
3282
+ * total budget still allows a further full-length attempt.
3283
+ */
3284
+ const CLIENT_DEADLINE_EXPIRY_MAX_RETRIES = 1;
3163
3285
  const log$l = (message, options = { type: "info" }) => {
3164
3286
  log$m(message, { ...options, source: "AlpacaMarketDataAPI" });
3165
3287
  };
@@ -3673,7 +3795,15 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
3673
3795
  // Retry ONLY transient connection faults, and only on GET (every
3674
3796
  // market-data read here is idempotent). A non-2xx response is a real
3675
3797
  // answer from Alpaca and is never retried — that path still throws on
3676
- // the first attempt exactly as before.
3798
+ // the first attempt exactly as before. The whole loop is bounded by a
3799
+ // cumulative deadline so retries can never stretch a hot-path read far
3800
+ // beyond a single attempt's timeout: connection-phase faults
3801
+ // (ECONNRESET class, millisecond-scale) retry cheaply, while a fault
3802
+ // that consumed the full client timeout retries at most once and only
3803
+ // when the remaining budget still fits a full-length attempt.
3804
+ const retryLoopStartedAt = Date.now();
3805
+ const totalBudgetMs = Math.round(DEFAULT_TIMEOUTS.ALPACA_API * TRANSIENT_RETRY_BUDGET_TIMEOUT_MULTIPLE);
3806
+ let deadlineExpiryRetries = 0;
3677
3807
  let response;
3678
3808
  let lastNetworkError;
3679
3809
  for (let attempt = 0; attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS; attempt += 1) {
@@ -3687,12 +3817,21 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
3687
3817
  }
3688
3818
  catch (networkErr) {
3689
3819
  lastNetworkError = networkErr;
3820
+ const deadlineExpiry = isClientDeadlineExpiry(networkErr);
3821
+ const nextAttemptFitsBudget = Date.now() - retryLoopStartedAt + DEFAULT_TIMEOUTS.ALPACA_API <=
3822
+ totalBudgetMs;
3690
3823
  const retryable = method === "GET" &&
3691
3824
  isTransientNetworkError(networkErr) &&
3692
- attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1;
3825
+ attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1 &&
3826
+ nextAttemptFitsBudget &&
3827
+ (!deadlineExpiry ||
3828
+ deadlineExpiryRetries < CLIENT_DEADLINE_EXPIRY_MAX_RETRIES);
3693
3829
  if (!retryable) {
3694
3830
  throw networkErr;
3695
3831
  }
3832
+ if (deadlineExpiry) {
3833
+ deadlineExpiryRetries += 1;
3834
+ }
3696
3835
  const delayMs = transientRetryDelayMs(attempt);
3697
3836
  log$l(`Transient network fault on ${endpoint} (attempt ${attempt + 1}/${TRANSIENT_NETWORK_RETRY_ATTEMPTS}); retrying in ${delayMs}ms`, { type: "warn" });
3698
3837
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -3713,8 +3852,7 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
3713
3852
  });
3714
3853
  throw new Error(`Market Data API error (${response.status}): ${errorText}`);
3715
3854
  }
3716
- const data = await response.json();
3717
- return data;
3855
+ return (await response.json());
3718
3856
  }
3719
3857
  catch (err) {
3720
3858
  const error = err;
@@ -4401,7 +4539,7 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
4401
4539
  });
4402
4540
  throw new Error(`Alpaca news API error (${response.status}): ${errorText}`);
4403
4541
  }
4404
- const data = await response.json();
4542
+ const data = (await response.json());
4405
4543
  if (!data.news || !Array.isArray(data.news)) {
4406
4544
  log$l(`No news data found in Alpaca response for ${symbol}`, {
4407
4545
  type: "warn",
@@ -4484,9 +4622,45 @@ const CLIENT_ORDER_ID_HASH_LENGTH = 32;
4484
4622
  * This derived default is a best-effort safety net; the guaranteed-idempotent
4485
4623
  * path is for the caller to pass an explicit `clientOrderId` tied to the
4486
4624
  * originating signal/decision id (which also permits legitimately-repeated
4487
- * identical orders inside a single window).
4625
+ * identical orders inside a single window). Callers that intentionally repeat
4626
+ * an identical order inside one window without managing explicit ids can pass
4627
+ * an `idempotencyNonce` (e.g. an attempt counter or signal id) instead — the
4628
+ * nonce is folded into the derived id, so each distinct nonce yields a
4629
+ * distinct id while a timeout+retry of the SAME attempt still collides
4630
+ * broker-side as intended.
4631
+ *
4632
+ * When a DERIVED id is 422-rejected as a duplicate, {@link
4633
+ * AlpacaTradingAPI.postOrderWithIdempotencyRecovery} recovers instead of
4634
+ * failing the caller: if the previously-submitted order is still live (or
4635
+ * filled) it is returned as idempotent success; if it is terminally dead
4636
+ * (canceled/expired/rejected) the order is resubmitted exactly once with a
4637
+ * fresh random salt. Caller-SUPPLIED ids are never recovered — they surface a
4638
+ * typed {@link DuplicateClientOrderIdError} so the caller can distinguish a
4639
+ * duplicate from a genuine rejection.
4488
4640
  */
4489
4641
  const CLIENT_ORDER_ID_WINDOW_MS = 300_000;
4642
+ /**
4643
+ * Matches Alpaca's 422 duplicate-idempotency-key rejection message. Alpaca has
4644
+ * used both "client_order_id must be unique" and "client order id must be
4645
+ * unique" across API revisions, so separators are matched loosely.
4646
+ */
4647
+ const DUPLICATE_CLIENT_ORDER_ID_PATTERN = /client[\s_-]?order[\s_-]?id must be unique/i;
4648
+ /** HTTP status Alpaca uses for duplicate `client_order_id` rejections. */
4649
+ const DUPLICATE_CLIENT_ORDER_ID_STATUS = 422;
4650
+ /**
4651
+ * Order statuses in which a previously-submitted order can never execute.
4652
+ * A derived-id duplicate colliding with an order in one of these states is a
4653
+ * legitimate NEW order (e.g. cancel-then-recreate of an identical trailing
4654
+ * stop) and is resubmitted with a fresh salt; any other status means the
4655
+ * original order is live or executed, so it is returned as idempotent success.
4656
+ */
4657
+ const TERMINAL_DEAD_ORDER_STATUSES = new Set([
4658
+ "canceled",
4659
+ "expired",
4660
+ "rejected",
4661
+ "replaced",
4662
+ "done_for_day",
4663
+ ]);
4490
4664
  /**
4491
4665
  Websocket example
4492
4666
  const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
@@ -4594,6 +4768,142 @@ class AlpacaTradingAPI {
4594
4768
  .slice(0, CLIENT_ORDER_ID_HASH_LENGTH);
4595
4769
  return `${CLIENT_ORDER_ID_PREFIX}${digest}`;
4596
4770
  }
4771
+ /**
4772
+ * Running count of derived-id duplicate collisions resolved by returning the
4773
+ * already-submitted order. Emitted in log metadata so operators can see the
4774
+ * idempotency net firing.
4775
+ */
4776
+ idempotentDuplicateReturns = 0;
4777
+ /**
4778
+ * Running count of derived-id duplicate collisions resolved by resubmitting
4779
+ * once with a fresh random salt (the colliding order was terminally dead).
4780
+ */
4781
+ saltedDuplicateResubmits = 0;
4782
+ /**
4783
+ * Whether an error thrown by {@link makeRequest} is Alpaca's 422
4784
+ * duplicate-`client_order_id` rejection.
4785
+ * @param error - The error thrown by the order POST.
4786
+ * @returns true when the error is a duplicate-idempotency-key rejection.
4787
+ */
4788
+ isDuplicateClientOrderIdRejection(error) {
4789
+ if (!(error instanceof Error))
4790
+ return false;
4791
+ return (error.message.includes(`(${DUPLICATE_CLIENT_ORDER_ID_STATUS})`) &&
4792
+ DUPLICATE_CLIENT_ORDER_ID_PATTERN.test(error.message));
4793
+ }
4794
+ /**
4795
+ * Look up an order by its `client_order_id` (Alpaca
4796
+ * `GET /orders:by_client_order_id`).
4797
+ * @param clientOrderId - The idempotency key the order was submitted with.
4798
+ * @returns The order, or null when no order exists for the id (404).
4799
+ */
4800
+ async getOrderByClientOrderId(clientOrderId) {
4801
+ try {
4802
+ return await this.makeRequest("/orders:by_client_order_id", "GET", undefined, `?client_order_id=${encodeURIComponent(clientOrderId)}`);
4803
+ }
4804
+ catch (error) {
4805
+ if (error instanceof Error && error.message.includes("(404)")) {
4806
+ return null;
4807
+ }
4808
+ throw error;
4809
+ }
4810
+ }
4811
+ /**
4812
+ * POST an order body with duplicate-`client_order_id` recovery.
4813
+ *
4814
+ * Sets `client_order_id` (explicit id wins; otherwise derived from
4815
+ * `deriveParts`), submits, and on Alpaca's 422 duplicate rejection:
4816
+ *
4817
+ * - **Caller-supplied id**: throws a typed
4818
+ * {@link DuplicateClientOrderIdError} — the caller owns idempotency
4819
+ * semantics and must decide whether the duplicate is success or a bug.
4820
+ * - **Derived id, colliding order live/filled**: returns the existing order
4821
+ * as idempotent success (this is the timeout+retry case the derived id
4822
+ * exists to de-duplicate).
4823
+ * - **Derived id, colliding order terminally dead** (canceled / expired /
4824
+ * rejected — e.g. cancel-then-recreate of an identical trailing stop):
4825
+ * resubmits exactly once with a fresh random salt so the legitimate new
4826
+ * order is not blocked. A second duplicate rejection throws the typed
4827
+ * error.
4828
+ * - **Derived id, status lookup fails**: fails CLOSED with the typed error —
4829
+ * the colliding order may be live, so resubmitting could double-fill; the
4830
+ * caller's next cycle retries when the lookup can succeed.
4831
+ *
4832
+ * Both recovery outcomes increment log-visible counters.
4833
+ *
4834
+ * @param body - The order payload (its `client_order_id` is set here).
4835
+ * @param options - Idempotency inputs: optional explicit id and the derive
4836
+ * parts used both for the default id and for the salted resubmit.
4837
+ * @returns The created (or pre-existing, on idempotent recovery) order.
4838
+ */
4839
+ async postOrderWithIdempotencyRecovery(body, options) {
4840
+ const derived = options.explicitClientOrderId === undefined;
4841
+ const clientOrderId = options.explicitClientOrderId ??
4842
+ this.deriveClientOrderId(options.deriveParts);
4843
+ body.client_order_id = clientOrderId;
4844
+ const requestBody = body;
4845
+ try {
4846
+ return await this.makeRequest("/orders", "POST", requestBody);
4847
+ }
4848
+ catch (error) {
4849
+ if (!this.isDuplicateClientOrderIdRejection(error)) {
4850
+ throw error;
4851
+ }
4852
+ if (!derived) {
4853
+ throw new DuplicateClientOrderIdError(`Duplicate client_order_id "${clientOrderId}" rejected by Alpaca (caller-supplied id)`, clientOrderId, false, error);
4854
+ }
4855
+ let existing = null;
4856
+ try {
4857
+ existing = await this.getOrderByClientOrderId(clientOrderId);
4858
+ }
4859
+ catch (lookupError) {
4860
+ // Fail CLOSED: the 422 proves an order with this id exists, but its
4861
+ // status is unverifiable. Resubmitting here could double-fill a live
4862
+ // order (the timeout+retry case), which is strictly worse than the
4863
+ // caller retrying on its next cycle — by then the lookup will resolve.
4864
+ this.log(`Duplicate-order lookup failed for ${clientOrderId}; failing closed (no resubmit) to avoid a possible double order: ${lookupError instanceof Error
4865
+ ? lookupError.message
4866
+ : String(lookupError)}`, { symbol: options.logSymbol, type: "error" });
4867
+ throw new DuplicateClientOrderIdError(`Duplicate client_order_id "${clientOrderId}" rejected by Alpaca and the existing-order lookup failed; refusing to resubmit (possible live duplicate)`, clientOrderId, true, lookupError);
4868
+ }
4869
+ if (existing && !TERMINAL_DEAD_ORDER_STATUSES.has(existing.status)) {
4870
+ this.idempotentDuplicateReturns++;
4871
+ this.log(`Derived client_order_id ${clientOrderId} already submitted (status=${existing.status}); returning existing order ${existing.id} as idempotent success`, {
4872
+ symbol: options.logSymbol,
4873
+ type: "warn",
4874
+ metadata: {
4875
+ outcome: "idempotent_return",
4876
+ idempotentDuplicateReturns: this.idempotentDuplicateReturns,
4877
+ },
4878
+ });
4879
+ return existing;
4880
+ }
4881
+ const saltedId = this.deriveClientOrderId([
4882
+ ...options.deriveParts,
4883
+ "resubmit",
4884
+ node_crypto.randomUUID(),
4885
+ ]);
4886
+ this.saltedDuplicateResubmits++;
4887
+ this.log(`Derived client_order_id ${clientOrderId} collided with a ${existing ? `terminal (${existing.status})` : "missing"} order; resubmitting once with fresh salt ${saltedId}`, {
4888
+ symbol: options.logSymbol,
4889
+ type: "warn",
4890
+ metadata: {
4891
+ outcome: "salted_resubmit",
4892
+ saltedDuplicateResubmits: this.saltedDuplicateResubmits,
4893
+ },
4894
+ });
4895
+ body.client_order_id = saltedId;
4896
+ try {
4897
+ return await this.makeRequest("/orders", "POST", requestBody);
4898
+ }
4899
+ catch (resubmitError) {
4900
+ if (this.isDuplicateClientOrderIdRejection(resubmitError)) {
4901
+ throw new DuplicateClientOrderIdError(`Salted resubmit of duplicate client_order_id "${clientOrderId}" was itself rejected as a duplicate ("${saltedId}")`, saltedId, true, resubmitError);
4902
+ }
4903
+ throw resubmitError;
4904
+ }
4905
+ }
4906
+ }
4597
4907
  /**
4598
4908
  * Collect the human-readable failure entries from a bulk Multi-Status (207)
4599
4909
  * response body (`DELETE /orders`, `DELETE /positions`). Each element carries
@@ -4896,7 +5206,7 @@ class AlpacaTradingAPI {
4896
5206
  }
4897
5207
  const contentType = response.headers.get("content-type");
4898
5208
  if (contentType && contentType.includes("application/json")) {
4899
- return await response.json();
5209
+ return (await response.json());
4900
5210
  }
4901
5211
  // For non-JSON responses, return the text content
4902
5212
  const textContent = await response.text();
@@ -5044,9 +5354,15 @@ class AlpacaTradingAPI {
5044
5354
  * @param side (string) - the side of the order
5045
5355
  * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
5046
5356
  * @param position_intent (string) - the position intent of the order
5357
+ * @param clientOrderId - Optional explicit idempotency key; when supplied it
5358
+ * is used verbatim and duplicate rejections surface as
5359
+ * {@link DuplicateClientOrderIdError}.
5360
+ * @param idempotencyNonce - Optional attempt/signal discriminator folded into
5361
+ * the derived idempotency key so an intentionally-repeated identical order
5362
+ * inside one derivation window receives a distinct id.
5047
5363
  * @returns The created AlpacaOrder with order ID and details
5048
5364
  */
5049
- async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId) {
5365
+ async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId, idempotencyNonce) {
5050
5366
  this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
5051
5367
  symbol,
5052
5368
  });
@@ -5059,18 +5375,21 @@ class AlpacaTradingAPI {
5059
5375
  type: "trailing_stop",
5060
5376
  trail_percent: trailPercent100.toString(), // Already in decimal form (e.g., 4 for 4%)
5061
5377
  time_in_force: "gtc",
5062
- client_order_id: clientOrderId ??
5063
- this.deriveClientOrderId([
5378
+ };
5379
+ try {
5380
+ const order = await this.postOrderWithIdempotencyRecovery(body, {
5381
+ explicitClientOrderId: clientOrderId,
5382
+ deriveParts: [
5064
5383
  "trailing_stop",
5065
5384
  symbol,
5066
5385
  side,
5067
5386
  position_intent,
5068
5387
  Math.abs(qty),
5069
5388
  trailPercent100,
5070
- ]),
5071
- };
5072
- try {
5073
- const order = await this.makeRequest(`/orders`, "POST", body);
5389
+ ...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
5390
+ ],
5391
+ logSymbol: symbol,
5392
+ });
5074
5393
  this.log(`Trailing stop order created for ${symbol}: orderId=${order.id}, trailPercent=${trailPercent100}%`, { symbol });
5075
5394
  return order;
5076
5395
  }
@@ -5088,8 +5407,14 @@ class AlpacaTradingAPI {
5088
5407
  * @param qty (number) - the quantity of the order
5089
5408
  * @param side (string) - the side of the order
5090
5409
  * @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
5410
+ * @param client_order_id - Optional explicit idempotency key; duplicate
5411
+ * rejections of an explicit id surface as
5412
+ * {@link DuplicateClientOrderIdError}.
5413
+ * @param idempotencyNonce - Optional attempt/signal discriminator folded into
5414
+ * the derived idempotency key so an intentionally-repeated identical order
5415
+ * inside one derivation window receives a distinct id.
5091
5416
  */
5092
- async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
5417
+ async createMarketOrder(symbol, qty, side, position_intent, client_order_id, idempotencyNonce) {
5093
5418
  this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
5094
5419
  symbol,
5095
5420
  });
@@ -5102,17 +5427,19 @@ class AlpacaTradingAPI {
5102
5427
  time_in_force: "day",
5103
5428
  order_class: "simple",
5104
5429
  };
5105
- body.client_order_id =
5106
- client_order_id ??
5107
- this.deriveClientOrderId([
5430
+ try {
5431
+ return await this.postOrderWithIdempotencyRecovery(body, {
5432
+ explicitClientOrderId: client_order_id,
5433
+ deriveParts: [
5108
5434
  "market",
5109
5435
  symbol,
5110
5436
  side,
5111
5437
  position_intent,
5112
5438
  Math.abs(qty),
5113
- ]);
5114
- try {
5115
- return await this.makeRequest("/orders", "POST", body);
5439
+ ...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
5440
+ ],
5441
+ logSymbol: symbol,
5442
+ });
5116
5443
  }
5117
5444
  catch (error) {
5118
5445
  this.log(`Error creating market order: ${error}`, { type: "error" });
@@ -5269,9 +5596,14 @@ class AlpacaTradingAPI {
5269
5596
  * @param limitPrice (number) - the limit price of the order
5270
5597
  * @param position_intent (string) - the position intent of the order
5271
5598
  * @param extended_hours (boolean) - whether the order is in extended hours
5272
- * @param client_order_id (string) - the client order id of the order
5599
+ * @param client_order_id - Optional explicit idempotency key; duplicate
5600
+ * rejections of an explicit id surface as
5601
+ * {@link DuplicateClientOrderIdError}.
5602
+ * @param idempotencyNonce - Optional attempt/signal discriminator folded into
5603
+ * the derived idempotency key so an intentionally-repeated identical order
5604
+ * inside one derivation window receives a distinct id.
5273
5605
  */
5274
- async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
5606
+ async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id, idempotencyNonce) {
5275
5607
  this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
5276
5608
  symbol,
5277
5609
  });
@@ -5286,9 +5618,10 @@ class AlpacaTradingAPI {
5286
5618
  order_class: "simple",
5287
5619
  extended_hours,
5288
5620
  };
5289
- body.client_order_id =
5290
- client_order_id ??
5291
- this.deriveClientOrderId([
5621
+ try {
5622
+ return await this.postOrderWithIdempotencyRecovery(body, {
5623
+ explicitClientOrderId: client_order_id,
5624
+ deriveParts: [
5292
5625
  "limit",
5293
5626
  symbol,
5294
5627
  side,
@@ -5296,9 +5629,10 @@ class AlpacaTradingAPI {
5296
5629
  Math.abs(qty),
5297
5630
  this.roundPriceForAlpaca(limitPrice),
5298
5631
  extended_hours,
5299
- ]);
5300
- try {
5301
- return await this.makeRequest("/orders", "POST", body);
5632
+ ...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
5633
+ ],
5634
+ logSymbol: symbol,
5635
+ });
5302
5636
  }
5303
5637
  catch (error) {
5304
5638
  this.log(`Error creating limit order: ${error}`, { type: "error" });
@@ -5451,10 +5785,14 @@ class AlpacaTradingAPI {
5451
5785
  * @param limitPrice Limit price (required for limit orders)
5452
5786
  * @param clientOrderId Optional idempotency key; a deterministic one is
5453
5787
  * derived from the order parameters when omitted so a client-timeout retry
5454
- * is de-duplicated broker-side.
5788
+ * is de-duplicated broker-side. Duplicate rejections of an explicit id
5789
+ * surface as {@link DuplicateClientOrderIdError}.
5790
+ * @param idempotencyNonce Optional attempt/signal discriminator folded into
5791
+ * the derived idempotency key so an intentionally-repeated identical order
5792
+ * inside one derivation window receives a distinct id.
5455
5793
  * @returns The created order
5456
5794
  */
5457
- async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId) {
5795
+ async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId, idempotencyNonce) {
5458
5796
  if (!Number.isInteger(qty) || qty <= 0) {
5459
5797
  this.log("Quantity must be a positive whole number for option orders", {
5460
5798
  type: "error",
@@ -5479,20 +5817,22 @@ class AlpacaTradingAPI {
5479
5817
  if (type === "limit" && limitPrice !== undefined) {
5480
5818
  orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
5481
5819
  }
5482
- orderData.client_order_id =
5483
- clientOrderId ??
5484
- this.deriveClientOrderId([
5485
- "option",
5486
- type,
5487
- symbol,
5488
- side,
5489
- position_intent,
5490
- qty,
5491
- type === "limit" && limitPrice !== undefined
5492
- ? this.roundPriceForAlpaca(limitPrice)
5493
- : undefined,
5494
- ]);
5495
- return this.makeRequest("/orders", "POST", orderData);
5820
+ return this.postOrderWithIdempotencyRecovery(orderData, {
5821
+ explicitClientOrderId: clientOrderId,
5822
+ deriveParts: [
5823
+ "option",
5824
+ type,
5825
+ symbol,
5826
+ side,
5827
+ position_intent,
5828
+ qty,
5829
+ type === "limit" && limitPrice !== undefined
5830
+ ? this.roundPriceForAlpaca(limitPrice)
5831
+ : undefined,
5832
+ ...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
5833
+ ],
5834
+ logSymbol: symbol,
5835
+ });
5496
5836
  }
5497
5837
  /**
5498
5838
  * Create a multi-leg option order
@@ -5502,10 +5842,14 @@ class AlpacaTradingAPI {
5502
5842
  * @param limitPrice Limit price (required for limit orders)
5503
5843
  * @param clientOrderId Optional idempotency key; a deterministic one is
5504
5844
  * derived from the legs and order parameters when omitted so a
5505
- * client-timeout retry is de-duplicated broker-side.
5845
+ * client-timeout retry is de-duplicated broker-side. Duplicate rejections
5846
+ * of an explicit id surface as {@link DuplicateClientOrderIdError}.
5847
+ * @param idempotencyNonce Optional attempt/signal discriminator folded into
5848
+ * the derived idempotency key so an intentionally-repeated identical order
5849
+ * inside one derivation window receives a distinct id.
5506
5850
  * @returns The created multi-leg order
5507
5851
  */
5508
- async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId) {
5852
+ async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId, idempotencyNonce) {
5509
5853
  if (!Number.isInteger(qty) || qty <= 0) {
5510
5854
  this.log("Quantity must be a positive whole number for option orders", {
5511
5855
  type: "error",
@@ -5531,18 +5875,20 @@ class AlpacaTradingAPI {
5531
5875
  if (type === "limit" && limitPrice !== undefined) {
5532
5876
  orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
5533
5877
  }
5534
- orderData.client_order_id =
5535
- clientOrderId ??
5536
- this.deriveClientOrderId([
5537
- "mleg",
5538
- type,
5539
- qty,
5540
- type === "limit" && limitPrice !== undefined
5541
- ? this.roundPriceForAlpaca(limitPrice)
5542
- : undefined,
5543
- ...legs.map((leg) => `${leg.symbol}:${leg.side}:${leg.ratio_qty}:${leg.position_intent}`),
5544
- ]);
5545
- return this.makeRequest("/orders", "POST", orderData);
5878
+ return this.postOrderWithIdempotencyRecovery(orderData, {
5879
+ explicitClientOrderId: clientOrderId,
5880
+ deriveParts: [
5881
+ "mleg",
5882
+ type,
5883
+ qty,
5884
+ type === "limit" && limitPrice !== undefined
5885
+ ? this.roundPriceForAlpaca(limitPrice)
5886
+ : undefined,
5887
+ ...legs.map((leg) => `${leg.symbol}:${leg.side}:${leg.ratio_qty}:${leg.position_intent}`),
5888
+ ...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
5889
+ ],
5890
+ logSymbol: legSymbols,
5891
+ });
5546
5892
  }
5547
5893
  /**
5548
5894
  * Exercise an option contract
@@ -5915,7 +6261,7 @@ class AlpacaTradingAPI {
5915
6261
  */
5916
6262
  async createEquitiesTrade(params, options) {
5917
6263
  const { symbol, qty, side, referencePrice } = params;
5918
- const { type = "market", limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
6264
+ const { type = "market", limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, idempotencyNonce, } = options || {};
5919
6265
  // Validation: Extended hours + market order is not allowed
5920
6266
  if (extendedHours && type === "market") {
5921
6267
  this.log("Cannot create market order with extended hours enabled", {
@@ -6019,26 +6365,25 @@ class AlpacaTradingAPI {
6019
6365
  extended_hours: extendedHours,
6020
6366
  position_intent: side === "buy" ? "buy_to_open" : "sell_to_open",
6021
6367
  };
6022
- orderData.client_order_id =
6023
- clientOrderId ??
6024
- this.deriveClientOrderId([
6025
- "equities",
6026
- orderClass,
6027
- type,
6028
- symbol,
6029
- side,
6030
- Math.abs(qty),
6031
- type === "limit" && limitPrice !== undefined
6032
- ? this.roundPriceForAlpaca(limitPrice)
6033
- : undefined,
6034
- extendedHours,
6035
- useStopLoss && calculatedStopPrice !== undefined
6036
- ? this.roundPriceForAlpaca(calculatedStopPrice)
6037
- : undefined,
6038
- useTakeProfit && calculatedTakeProfitPrice !== undefined
6039
- ? this.roundPriceForAlpaca(calculatedTakeProfitPrice)
6040
- : undefined,
6041
- ]);
6368
+ const deriveParts = [
6369
+ "equities",
6370
+ orderClass,
6371
+ type,
6372
+ symbol,
6373
+ side,
6374
+ Math.abs(qty),
6375
+ type === "limit" && limitPrice !== undefined
6376
+ ? this.roundPriceForAlpaca(limitPrice)
6377
+ : undefined,
6378
+ extendedHours,
6379
+ useStopLoss && calculatedStopPrice !== undefined
6380
+ ? this.roundPriceForAlpaca(calculatedStopPrice)
6381
+ : undefined,
6382
+ useTakeProfit && calculatedTakeProfitPrice !== undefined
6383
+ ? this.roundPriceForAlpaca(calculatedTakeProfitPrice)
6384
+ : undefined,
6385
+ ...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
6386
+ ];
6042
6387
  // Add limit price for limit orders
6043
6388
  if (type === "limit" && limitPrice !== undefined) {
6044
6389
  orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
@@ -6062,7 +6407,11 @@ class AlpacaTradingAPI {
6062
6407
  symbol,
6063
6408
  });
6064
6409
  try {
6065
- return await this.makeRequest("/orders", "POST", orderData);
6410
+ return await this.postOrderWithIdempotencyRecovery(orderData, {
6411
+ explicitClientOrderId: clientOrderId,
6412
+ deriveParts,
6413
+ logSymbol: symbol,
6414
+ });
6066
6415
  }
6067
6416
  catch (error) {
6068
6417
  this.log(`Error creating equities trade: ${error}`, {
@@ -11045,7 +11394,9 @@ async function calculateTotalReturnYTD(portfolioHistory) {
11045
11394
  * @param accountId - The ID of the Alpaca account.
11046
11395
  * @param client - The Apollo client instance.
11047
11396
  * @param alpacaAccount - The Alpaca account object.
11048
- * @returns A promise that resolves to a string representing the expense ratio in percentage format.
11397
+ * @returns A promise that resolves to a string representing the expense ratio
11398
+ * in percentage format, or "N/A" when the ratio cannot be computed honestly
11399
+ * (missing account, fee-fetch failure, or non-positive/non-finite equity).
11049
11400
  */
11050
11401
  async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
11051
11402
  if (!accountId && !alpacaAccount && !client) {
@@ -11079,12 +11430,15 @@ async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
11079
11430
  return "N/A";
11080
11431
  }
11081
11432
  }
11082
- // Validate equity
11083
- if (!accountDetails.equity || isNaN(parseFloat(accountDetails.equity))) {
11084
- getLogger().warn("Invalid equity value.");
11433
+ // Validate equity. A drained or freshly-funded account reports equity "0"
11434
+ // (and a broken feed can yield NaN or a negative string); dividing by it
11435
+ // would fabricate "Infinity%"/"NaN%" — return the deliberate "N/A"
11436
+ // (unknown) instead, consistent with the fee-fetch failure path below.
11437
+ const equity = parseFloat(accountDetails.equity);
11438
+ if (!Number.isFinite(equity) || equity <= 0) {
11439
+ getLogger().warn("Non-positive or non-finite equity value; cannot compute expense ratio.", { equity: accountDetails.equity });
11085
11440
  return "N/A";
11086
11441
  }
11087
- const equity = parseFloat(accountDetails.equity);
11088
11442
  // Fetch the account's real trailing fee expenses from Alpaca account
11089
11443
  // activities. A genuine data-source failure yields "N/A" (unknown) rather
11090
11444
  // than a fabricated 0.00%.
@@ -11410,8 +11764,13 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
11410
11764
  const alignedPortfolioReturns = [];
11411
11765
  const alignedBenchmarkReturns = [];
11412
11766
  for (const timestamp of commonTimestamps) {
11767
+ // commonTimestamps is the key intersection of both maps, so both lookups
11768
+ // are guaranteed present; the guard replaces a non-null assertion.
11413
11769
  const portfolioRet = portfolioReturnsMap.get(timestamp);
11414
11770
  const benchmarkRet = benchmarkReturnsMap.get(timestamp);
11771
+ if (portfolioRet === undefined || benchmarkRet === undefined) {
11772
+ continue;
11773
+ }
11415
11774
  if (isFinite(portfolioRet) && isFinite(benchmarkRet)) {
11416
11775
  alignedPortfolioReturns.push(portfolioRet);
11417
11776
  alignedBenchmarkReturns.push(benchmarkRet);
@@ -11672,8 +12031,13 @@ function alignReturnsByDate(portfolioHistory, benchmarkBars) {
11672
12031
  const alignedPortfolioReturns = [];
11673
12032
  const alignedBenchmarkReturns = [];
11674
12033
  for (const timestamp of commonTimestamps) {
12034
+ // commonTimestamps is the key intersection of both maps, so both lookups
12035
+ // are guaranteed present; the guard replaces a non-null assertion.
11675
12036
  const portfolioRet = portfolioReturnsMap.get(timestamp);
11676
12037
  const benchmarkRet = benchmarkReturnsMap.get(timestamp);
12038
+ if (portfolioRet === undefined || benchmarkRet === undefined) {
12039
+ continue;
12040
+ }
11677
12041
  alignedPortfolioReturns.push(portfolioRet);
11678
12042
  alignedBenchmarkReturns.push(benchmarkRet);
11679
12043
  }
@@ -11790,8 +12154,13 @@ async function calculateInformationRatio(portfolioHistory, benchmarkBars) {
11790
12154
  // Extract aligned returns
11791
12155
  const activeReturns = [];
11792
12156
  for (const timestamp of commonTimestamps) {
12157
+ // commonTimestamps is the key intersection of both maps, so both lookups
12158
+ // are guaranteed present; the guard replaces a non-null assertion.
11793
12159
  const portfolioRet = portfolioReturnsMap.get(timestamp);
11794
12160
  const benchmarkRet = benchmarkReturnsMap.get(timestamp);
12161
+ if (portfolioRet === undefined || benchmarkRet === undefined) {
12162
+ continue;
12163
+ }
11795
12164
  activeReturns.push(portfolioRet - benchmarkRet);
11796
12165
  }
11797
12166
  const n = activeReturns.length;
@@ -14620,17 +14989,17 @@ var hasRequiredBrowser;
14620
14989
  function requireBrowser () {
14621
14990
  if (hasRequiredBrowser) return browser.exports;
14622
14991
  hasRequiredBrowser = 1;
14623
- (function (module, exports$1) {
14992
+ (function (module, exports) {
14624
14993
  /**
14625
14994
  * This is the web browser implementation of `debug()`.
14626
14995
  */
14627
14996
 
14628
- exports$1.formatArgs = formatArgs;
14629
- exports$1.save = save;
14630
- exports$1.load = load;
14631
- exports$1.useColors = useColors;
14632
- exports$1.storage = localstorage();
14633
- exports$1.destroy = (() => {
14997
+ exports.formatArgs = formatArgs;
14998
+ exports.save = save;
14999
+ exports.load = load;
15000
+ exports.useColors = useColors;
15001
+ exports.storage = localstorage();
15002
+ exports.destroy = (() => {
14634
15003
  let warned = false;
14635
15004
 
14636
15005
  return () => {
@@ -14645,7 +15014,7 @@ function requireBrowser () {
14645
15014
  * Colors.
14646
15015
  */
14647
15016
 
14648
- exports$1.colors = [
15017
+ exports.colors = [
14649
15018
  '#0000CC',
14650
15019
  '#0000FF',
14651
15020
  '#0033CC',
@@ -14810,7 +15179,7 @@ function requireBrowser () {
14810
15179
  *
14811
15180
  * @api public
14812
15181
  */
14813
- exports$1.log = console.debug || console.log || (() => {});
15182
+ exports.log = console.debug || console.log || (() => {});
14814
15183
 
14815
15184
  /**
14816
15185
  * Save `namespaces`.
@@ -14821,9 +15190,9 @@ function requireBrowser () {
14821
15190
  function save(namespaces) {
14822
15191
  try {
14823
15192
  if (namespaces) {
14824
- exports$1.storage.setItem('debug', namespaces);
15193
+ exports.storage.setItem('debug', namespaces);
14825
15194
  } else {
14826
- exports$1.storage.removeItem('debug');
15195
+ exports.storage.removeItem('debug');
14827
15196
  }
14828
15197
  } catch (error) {
14829
15198
  // Swallow
@@ -14840,7 +15209,7 @@ function requireBrowser () {
14840
15209
  function load() {
14841
15210
  let r;
14842
15211
  try {
14843
- r = exports$1.storage.getItem('debug') || exports$1.storage.getItem('DEBUG') ;
15212
+ r = exports.storage.getItem('debug') || exports.storage.getItem('DEBUG') ;
14844
15213
  } catch (error) {
14845
15214
  // Swallow
14846
15215
  // XXX (@Qix-) should we be logging these?
@@ -14876,7 +15245,7 @@ function requireBrowser () {
14876
15245
  }
14877
15246
  }
14878
15247
 
14879
- module.exports = requireCommon()(exports$1);
15248
+ module.exports = requireCommon()(exports);
14880
15249
 
14881
15250
  const {formatters} = module.exports;
14882
15251
 
@@ -15065,7 +15434,7 @@ var hasRequiredNode$1;
15065
15434
  function requireNode$1 () {
15066
15435
  if (hasRequiredNode$1) return node$1.exports;
15067
15436
  hasRequiredNode$1 = 1;
15068
- (function (module, exports$1) {
15437
+ (function (module, exports) {
15069
15438
  const tty = require$$1$1;
15070
15439
  const util = require$$1$2;
15071
15440
 
@@ -15073,13 +15442,13 @@ function requireNode$1 () {
15073
15442
  * This is the Node.js implementation of `debug()`.
15074
15443
  */
15075
15444
 
15076
- exports$1.init = init;
15077
- exports$1.log = log;
15078
- exports$1.formatArgs = formatArgs;
15079
- exports$1.save = save;
15080
- exports$1.load = load;
15081
- exports$1.useColors = useColors;
15082
- exports$1.destroy = util.deprecate(
15445
+ exports.init = init;
15446
+ exports.log = log;
15447
+ exports.formatArgs = formatArgs;
15448
+ exports.save = save;
15449
+ exports.load = load;
15450
+ exports.useColors = useColors;
15451
+ exports.destroy = util.deprecate(
15083
15452
  () => {},
15084
15453
  'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'
15085
15454
  );
@@ -15088,7 +15457,7 @@ function requireNode$1 () {
15088
15457
  * Colors.
15089
15458
  */
15090
15459
 
15091
- exports$1.colors = [6, 2, 3, 4, 5, 1];
15460
+ exports.colors = [6, 2, 3, 4, 5, 1];
15092
15461
 
15093
15462
  try {
15094
15463
  // Optional dependency (as in, doesn't need to be installed, NOT like optionalDependencies in package.json)
@@ -15096,7 +15465,7 @@ function requireNode$1 () {
15096
15465
  const supportsColor = requireSupportsColor();
15097
15466
 
15098
15467
  if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
15099
- exports$1.colors = [
15468
+ exports.colors = [
15100
15469
  20,
15101
15470
  21,
15102
15471
  26,
@@ -15185,7 +15554,7 @@ function requireNode$1 () {
15185
15554
  * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
15186
15555
  */
15187
15556
 
15188
- exports$1.inspectOpts = Object.keys(process.env).filter(key => {
15557
+ exports.inspectOpts = Object.keys(process.env).filter(key => {
15189
15558
  return /^debug_/i.test(key);
15190
15559
  }).reduce((obj, key) => {
15191
15560
  // Camel-case
@@ -15217,8 +15586,8 @@ function requireNode$1 () {
15217
15586
  */
15218
15587
 
15219
15588
  function useColors() {
15220
- return 'colors' in exports$1.inspectOpts ?
15221
- Boolean(exports$1.inspectOpts.colors) :
15589
+ return 'colors' in exports.inspectOpts ?
15590
+ Boolean(exports.inspectOpts.colors) :
15222
15591
  tty.isatty(process.stderr.fd);
15223
15592
  }
15224
15593
 
@@ -15244,7 +15613,7 @@ function requireNode$1 () {
15244
15613
  }
15245
15614
 
15246
15615
  function getDate() {
15247
- if (exports$1.inspectOpts.hideDate) {
15616
+ if (exports.inspectOpts.hideDate) {
15248
15617
  return '';
15249
15618
  }
15250
15619
  return new Date().toISOString() + ' ';
@@ -15255,7 +15624,7 @@ function requireNode$1 () {
15255
15624
  */
15256
15625
 
15257
15626
  function log(...args) {
15258
- return process.stderr.write(util.formatWithOptions(exports$1.inspectOpts, ...args) + '\n');
15627
+ return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + '\n');
15259
15628
  }
15260
15629
 
15261
15630
  /**
@@ -15295,13 +15664,13 @@ function requireNode$1 () {
15295
15664
  function init(debug) {
15296
15665
  debug.inspectOpts = {};
15297
15666
 
15298
- const keys = Object.keys(exports$1.inspectOpts);
15667
+ const keys = Object.keys(exports.inspectOpts);
15299
15668
  for (let i = 0; i < keys.length; i++) {
15300
- debug.inspectOpts[keys[i]] = exports$1.inspectOpts[keys[i]];
15669
+ debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
15301
15670
  }
15302
15671
  }
15303
15672
 
15304
- module.exports = requireCommon()(exports$1);
15673
+ module.exports = requireCommon()(exports);
15305
15674
 
15306
15675
  const {formatters} = module.exports;
15307
15676
 
@@ -15875,7 +16244,7 @@ function requireFollowRedirects () {
15875
16244
  // Wraps the key/value object of protocols with redirect functionality
15876
16245
  function wrap(protocols) {
15877
16246
  // Default settings
15878
- var exports$1 = {
16247
+ var exports = {
15879
16248
  maxRedirects: 21,
15880
16249
  maxBodyLength: 10 * 1024 * 1024,
15881
16250
  };
@@ -15885,7 +16254,7 @@ function requireFollowRedirects () {
15885
16254
  Object.keys(protocols).forEach(function (scheme) {
15886
16255
  var protocol = scheme + ":";
15887
16256
  var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
15888
- var wrappedProtocol = exports$1[scheme] = Object.create(nativeProtocol);
16257
+ var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
15889
16258
 
15890
16259
  // Executes a request, following redirects
15891
16260
  function request(input, options, callback) {
@@ -15908,8 +16277,8 @@ function requireFollowRedirects () {
15908
16277
 
15909
16278
  // Set defaults
15910
16279
  options = Object.assign({
15911
- maxRedirects: exports$1.maxRedirects,
15912
- maxBodyLength: exports$1.maxBodyLength,
16280
+ maxRedirects: exports.maxRedirects,
16281
+ maxBodyLength: exports.maxBodyLength,
15913
16282
  }, input, options);
15914
16283
  options.nativeProtocols = nativeProtocols;
15915
16284
  if (!isString(options.host) && !isString(options.hostname)) {
@@ -15934,7 +16303,7 @@ function requireFollowRedirects () {
15934
16303
  get: { value: get, configurable: true, enumerable: true, writable: true },
15935
16304
  });
15936
16305
  });
15937
- return exports$1;
16306
+ return exports;
15938
16307
  }
15939
16308
 
15940
16309
  function noop() { /* empty */ }
@@ -17540,7 +17909,7 @@ var hasRequiredLodash;
17540
17909
  function requireLodash () {
17541
17910
  if (hasRequiredLodash) return lodash$1.exports;
17542
17911
  hasRequiredLodash = 1;
17543
- (function (module, exports$1) {
17912
+ (function (module, exports) {
17544
17913
  (function() {
17545
17914
 
17546
17915
  /** Used as a safe reference for `undefined` in pre-ES5 environments. */
@@ -17971,7 +18340,7 @@ function requireLodash () {
17971
18340
  var root = freeGlobal || freeSelf || Function('return this')();
17972
18341
 
17973
18342
  /** Detect free variable `exports`. */
17974
- var freeExports = exports$1 && !exports$1.nodeType && exports$1;
18343
+ var freeExports = exports && !exports.nodeType && exports;
17975
18344
 
17976
18345
  /** Detect free variable `module`. */
17977
18346
  var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
@@ -35801,12 +36170,12 @@ var hasRequiredIsBuffer;
35801
36170
  function requireIsBuffer () {
35802
36171
  if (hasRequiredIsBuffer) return isBuffer.exports;
35803
36172
  hasRequiredIsBuffer = 1;
35804
- (function (module, exports$1) {
36173
+ (function (module, exports) {
35805
36174
  var root = require_root(),
35806
36175
  stubFalse = requireStubFalse();
35807
36176
 
35808
36177
  /** Detect free variable `exports`. */
35809
- var freeExports = exports$1 && !exports$1.nodeType && exports$1;
36178
+ var freeExports = exports && !exports.nodeType && exports;
35810
36179
 
35811
36180
  /** Detect free variable `module`. */
35812
36181
  var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
@@ -36026,11 +36395,11 @@ var hasRequired_nodeUtil;
36026
36395
  function require_nodeUtil () {
36027
36396
  if (hasRequired_nodeUtil) return _nodeUtil.exports;
36028
36397
  hasRequired_nodeUtil = 1;
36029
- (function (module, exports$1) {
36398
+ (function (module, exports) {
36030
36399
  var freeGlobal = require_freeGlobal();
36031
36400
 
36032
36401
  /** Detect free variable `exports`. */
36033
- var freeExports = exports$1 && !exports$1.nodeType && exports$1;
36402
+ var freeExports = exports && !exports.nodeType && exports;
36034
36403
 
36035
36404
  /** Detect free variable `module`. */
36036
36405
  var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
@@ -44952,7 +45321,7 @@ var hasRequiredSafeBuffer;
44952
45321
  function requireSafeBuffer () {
44953
45322
  if (hasRequiredSafeBuffer) return safeBuffer.exports;
44954
45323
  hasRequiredSafeBuffer = 1;
44955
- (function (module, exports$1) {
45324
+ (function (module, exports) {
44956
45325
  /* eslint-disable node/no-deprecated-api */
44957
45326
  var buffer = require$$0$5;
44958
45327
  var Buffer = buffer.Buffer;
@@ -44967,8 +45336,8 @@ function requireSafeBuffer () {
44967
45336
  module.exports = buffer;
44968
45337
  } else {
44969
45338
  // Copy properties from require('buffer')
44970
- copyProps(buffer, exports$1);
44971
- exports$1.Buffer = SafeBuffer;
45339
+ copyProps(buffer, exports);
45340
+ exports.Buffer = SafeBuffer;
44972
45341
  }
44973
45342
 
44974
45343
  function SafeBuffer (arg, encodingOrOffset, length) {
@@ -48166,22 +48535,22 @@ var hasRequiredReadable;
48166
48535
  function requireReadable () {
48167
48536
  if (hasRequiredReadable) return readable.exports;
48168
48537
  hasRequiredReadable = 1;
48169
- (function (module, exports$1) {
48538
+ (function (module, exports) {
48170
48539
  var Stream = require$$0$4;
48171
48540
  if (process.env.READABLE_STREAM === 'disable' && Stream) {
48172
48541
  module.exports = Stream.Readable;
48173
48542
  Object.assign(module.exports, Stream);
48174
48543
  module.exports.Stream = Stream;
48175
48544
  } else {
48176
- exports$1 = module.exports = require_stream_readable();
48177
- exports$1.Stream = Stream || exports$1;
48178
- exports$1.Readable = exports$1;
48179
- exports$1.Writable = require_stream_writable();
48180
- exports$1.Duplex = require_stream_duplex();
48181
- exports$1.Transform = require_stream_transform();
48182
- exports$1.PassThrough = require_stream_passthrough();
48183
- exports$1.finished = requireEndOfStream();
48184
- exports$1.pipeline = requirePipeline();
48545
+ exports = module.exports = require_stream_readable();
48546
+ exports.Stream = Stream || exports;
48547
+ exports.Readable = exports;
48548
+ exports.Writable = require_stream_writable();
48549
+ exports.Duplex = require_stream_duplex();
48550
+ exports.Transform = require_stream_transform();
48551
+ exports.PassThrough = require_stream_passthrough();
48552
+ exports.finished = requireEndOfStream();
48553
+ exports.pipeline = requirePipeline();
48185
48554
  }
48186
48555
  } (readable, readable.exports));
48187
48556
  return readable.exports;
@@ -49616,12 +49985,12 @@ var hasRequiredWebsocket;
49616
49985
  function requireWebsocket () {
49617
49986
  if (hasRequiredWebsocket) return websocket$1;
49618
49987
  hasRequiredWebsocket = 1;
49619
- (function (exports$1) {
49988
+ (function (exports) {
49620
49989
  var __importDefault = (websocket$1 && websocket$1.__importDefault) || function (mod) {
49621
49990
  return (mod && mod.__esModule) ? mod : { "default": mod };
49622
49991
  };
49623
- Object.defineProperty(exports$1, "__esModule", { value: true });
49624
- exports$1.AlpacaWebsocket = exports$1.ERROR = exports$1.CONN_ERROR = exports$1.EVENT = exports$1.STATE = void 0;
49992
+ Object.defineProperty(exports, "__esModule", { value: true });
49993
+ exports.AlpacaWebsocket = exports.ERROR = exports.CONN_ERROR = exports.EVENT = exports.STATE = void 0;
49625
49994
  const events_1 = __importDefault(require$$0$1);
49626
49995
  const ws_1 = __importDefault(requireWs());
49627
49996
  const msgpack5_1 = __importDefault(requireMsgpack5());
@@ -49635,7 +50004,7 @@ function requireWebsocket () {
49635
50004
  STATE["DISCONNECTED"] = "disconnected";
49636
50005
  STATE["WAITING_TO_CONNECT"] = "waiting to connect";
49637
50006
  STATE["WAITING_TO_RECONNECT"] = "waiting to reconnect";
49638
- })(STATE || (exports$1.STATE = STATE = {}));
50007
+ })(STATE || (exports.STATE = STATE = {}));
49639
50008
  // Client events
49640
50009
  var EVENT;
49641
50010
  (function (EVENT) {
@@ -49654,9 +50023,9 @@ function requireWebsocket () {
49654
50023
  EVENT["CORRECTIONS"] = "corrections";
49655
50024
  EVENT["ORDERBOOKS"] = "orderbooks";
49656
50025
  EVENT["NEWS"] = "news";
49657
- })(EVENT || (exports$1.EVENT = EVENT = {}));
50026
+ })(EVENT || (exports.EVENT = EVENT = {}));
49658
50027
  // Connection errors by code
49659
- exports$1.CONN_ERROR = new Map([
50028
+ exports.CONN_ERROR = new Map([
49660
50029
  [400, "invalid syntax"],
49661
50030
  [401, "not authenticated"],
49662
50031
  [402, "auth failed"],
@@ -49675,7 +50044,7 @@ function requireWebsocket () {
49675
50044
  ERROR["MISSING_SECERT_KEY"] = "missing secret key";
49676
50045
  ERROR["MISSING_API_KEY"] = "missing api key";
49677
50046
  ERROR["UNEXPECTED_MESSAGE"] = "unexpected message";
49678
- })(ERROR || (exports$1.ERROR = ERROR = {}));
50047
+ })(ERROR || (exports.ERROR = ERROR = {}));
49679
50048
  class AlpacaWebsocket extends events_1.default.EventEmitter {
49680
50049
  constructor(options) {
49681
50050
  super();
@@ -49832,7 +50201,7 @@ function requireWebsocket () {
49832
50201
  this.updateSubscriptions(data[0]);
49833
50202
  break;
49834
50203
  case "error":
49835
- this.emit(EVENT.CLIENT_ERROR, exports$1.CONN_ERROR.get(data[0].code));
50204
+ this.emit(EVENT.CLIENT_ERROR, exports.CONN_ERROR.get(data[0].code));
49836
50205
  break;
49837
50206
  default:
49838
50207
  this.dataHandler(data);
@@ -49861,7 +50230,7 @@ function requireWebsocket () {
49861
50230
  }
49862
50231
  }
49863
50232
  }
49864
- exports$1.AlpacaWebsocket = AlpacaWebsocket;
50233
+ exports.AlpacaWebsocket = AlpacaWebsocket;
49865
50234
  } (websocket$1));
49866
50235
  return websocket$1;
49867
50236
  }
@@ -50495,8 +50864,8 @@ var hasRequiredWebsockets;
50495
50864
  function requireWebsockets () {
50496
50865
  if (hasRequiredWebsockets) return websockets;
50497
50866
  hasRequiredWebsockets = 1;
50498
- (function (exports$1) {
50499
- Object.defineProperty(exports$1, "__esModule", { value: true });
50867
+ (function (exports) {
50868
+ Object.defineProperty(exports, "__esModule", { value: true });
50500
50869
  const events = require$$0$1;
50501
50870
  const WebSocket = requireWs();
50502
50871
  const entity = requireEntity();
@@ -50511,7 +50880,7 @@ function requireWebsockets () {
50511
50880
  STATE.DISCONNECTED = "disconnected";
50512
50881
  STATE.WAITING_TO_CONNECT = "waiting to connect";
50513
50882
  STATE.WAITING_TO_RECONNECT = "waiting to reconnect";
50514
- })((STATE = exports$1.STATE || (exports$1.STATE = {})));
50883
+ })((STATE = exports.STATE || (exports.STATE = {})));
50515
50884
  // Client events
50516
50885
  var EVENT;
50517
50886
  (function (EVENT) {
@@ -50525,7 +50894,7 @@ function requireWebsockets () {
50525
50894
  EVENT.STOCK_QUOTES = "stock_quotes";
50526
50895
  EVENT.STOCK_AGG_SEC = "stock_agg_sec";
50527
50896
  EVENT.STOCK_AGG_MIN = "stock_agg_min";
50528
- })((EVENT = exports$1.EVENT || (exports$1.EVENT = {})));
50897
+ })((EVENT = exports.EVENT || (exports.EVENT = {})));
50529
50898
  // Connection errors Each of these will also emit EVENT.ERROR
50530
50899
  var ERROR;
50531
50900
  (function (ERROR) {
@@ -50534,7 +50903,7 @@ function requireWebsockets () {
50534
50903
  ERROR.MISSING_API_KEY = "missing api key";
50535
50904
  ERROR.MISSING_SECRET_KEY = "missing secret key";
50536
50905
  ERROR.UNKNOWN = "unknown error";
50537
- })((ERROR = exports$1.ERROR || (exports$1.ERROR = {})));
50906
+ })((ERROR = exports.ERROR || (exports.ERROR = {})));
50538
50907
  /**
50539
50908
  * AlpacaStreamClient manages a connection to Alpaca's websocket api
50540
50909
  */
@@ -50847,7 +51216,7 @@ function requireWebsockets () {
50847
51216
  }
50848
51217
  }
50849
51218
  }
50850
- exports$1.AlpacaStreamClient = AlpacaStreamClient;
51219
+ exports.AlpacaStreamClient = AlpacaStreamClient;
50851
51220
  } (websockets));
50852
51221
  return websockets;
50853
51222
  }
@@ -51099,13 +51468,13 @@ var hasRequiredDist;
51099
51468
  function requireDist () {
51100
51469
  if (hasRequiredDist) return dist$1.exports;
51101
51470
  hasRequiredDist = 1;
51102
- (function (module, exports$1) {
51471
+ (function (module, exports) {
51103
51472
  var __importDefault = (dist && dist.__importDefault) || function (mod) {
51104
51473
  return (mod && mod.__esModule) ? mod : { "default": mod };
51105
51474
  };
51106
- Object.defineProperty(exports$1, "__esModule", { value: true });
51475
+ Object.defineProperty(exports, "__esModule", { value: true });
51107
51476
  const alpaca_trade_api_1 = __importDefault(requireAlpacaTradeApi());
51108
- exports$1.default = alpaca_trade_api_1.default;
51477
+ exports.default = alpaca_trade_api_1.default;
51109
51478
  module.exports = alpaca_trade_api_1.default;
51110
51479
  } (dist$1, dist$1.exports));
51111
51480
  return dist$1.exports;
@@ -63149,6 +63518,14 @@ class LRUCache {
63149
63518
  }
63150
63519
  }
63151
63520
 
63521
+ /**
63522
+ * Default hard ceiling (ms) on a single loader invocation. Resolved at
63523
+ * construction time into `options.loadTimeoutMs` so the effective value is
63524
+ * always a validated number (see the constructor guard) — mirrors the
63525
+ * engine-local copy's constructor-time resolution ahead of consolidating the
63526
+ * two implementations onto this one.
63527
+ */
63528
+ const DEFAULT_LOAD_TIMEOUT_MS = 30_000;
63152
63529
  /**
63153
63530
  * StampedeProtectedCache provides three-layer protection against cache stampedes
63154
63531
  *
@@ -63217,12 +63594,17 @@ class StampedeProtectedCache {
63217
63594
  loadTimeouts: 0,
63218
63595
  };
63219
63596
  constructor(options) {
63597
+ if (options.loadTimeoutMs !== undefined &&
63598
+ (!Number.isFinite(options.loadTimeoutMs) || options.loadTimeoutMs <= 0)) {
63599
+ throw new RangeError(`StampedeProtectedCache loadTimeoutMs must be a positive finite number of milliseconds; received ${String(options.loadTimeoutMs)}`);
63600
+ }
63220
63601
  this.options = {
63221
63602
  ...options,
63222
63603
  staleWhileRevalidateTtl: options.staleWhileRevalidateTtl ?? options.defaultTtl * 2,
63223
63604
  minJitter: options.minJitter ?? 0.9,
63224
63605
  maxJitter: options.maxJitter ?? 1.1,
63225
63606
  enableBackgroundRefresh: options.enableBackgroundRefresh ?? true,
63607
+ loadTimeoutMs: options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS,
63226
63608
  logger: options.logger ?? {
63227
63609
  debug: () => { },
63228
63610
  info: () => { },
@@ -63299,7 +63681,7 @@ class StampedeProtectedCache {
63299
63681
  cached.accessCount++;
63300
63682
  cached.lastAccessedAt = now;
63301
63683
  // Check if entry is still fresh (considering probabilistic expiration)
63302
- const jitteredExpiresAt = this.applyJitter(cached.expiresAt);
63684
+ const jitteredExpiresAt = this.applyJitter(cached.expiresAt, cached.ttl);
63303
63685
  if (now < jitteredExpiresAt) {
63304
63686
  // Fresh hit
63305
63687
  this.stats.hits++;
@@ -63562,12 +63944,16 @@ class StampedeProtectedCache {
63562
63944
  * is logged rather than surfacing as an unhandled rejection.
63563
63945
  */
63564
63946
  async loadWithTimeout(key, loader, ttl) {
63565
- const timeoutMs = this.options.loadTimeoutMs ?? 30000;
63566
- const loadPromise = this.loadAndCache(key, loader, ttl);
63947
+ const timeoutMs = this.options.loadTimeoutMs;
63948
+ const invocation = { abandoned: false };
63949
+ const loadPromise = this.loadAndCache(key, loader, ttl, invocation);
63567
63950
  let timeoutHandle;
63568
63951
  const timeoutPromise = new Promise((_, reject) => {
63569
63952
  timeoutHandle = setTimeout(() => {
63570
63953
  this.stats.loadTimeouts++;
63954
+ // Mark the invocation abandoned BEFORE evicting the pin: a retry that
63955
+ // starts now must never be overwritten by this loader's late result.
63956
+ invocation.abandoned = true;
63571
63957
  // Evict the pin so the next caller retries fresh.
63572
63958
  this.pendingRefreshes.delete(key);
63573
63959
  this.options.logger.warn("Cache loader timed out — pin evicted", {
@@ -63600,11 +63986,19 @@ class StampedeProtectedCache {
63600
63986
  /**
63601
63987
  * Load data and cache it
63602
63988
  */
63603
- async loadAndCache(key, loader, ttl) {
63989
+ async loadAndCache(key, loader, ttl, invocation = { abandoned: false }) {
63604
63990
  const startTime = Date.now();
63605
63991
  try {
63606
63992
  this.options.logger.debug("Loading data", { key });
63607
63993
  const value = await loader(key);
63994
+ if (invocation.abandoned) {
63995
+ // The pin was evicted at loadTimeoutMs and a retry may have cached
63996
+ // fresher data since; writing this late value would overwrite it with
63997
+ // a snapshot fetched before/through the hang.
63998
+ const loadTime = Date.now() - startTime;
63999
+ this.options.logger.warn("Abandoned cache loader resolved late — result discarded", { key, loadTime });
64000
+ return value;
64001
+ }
63608
64002
  // Cache the loaded value
63609
64003
  this.set(key, value, ttl);
63610
64004
  const loadTime = Date.now() - startTime;
@@ -63619,11 +64013,15 @@ class StampedeProtectedCache {
63619
64013
  error,
63620
64014
  loadTime,
63621
64015
  });
63622
- // Update cached entry with error if it exists
63623
- const cached = this.cache.get(key);
63624
- if (cached) {
63625
- cached.lastError = error;
63626
- cached.isRefreshing = false;
64016
+ // Update cached entry with error if it exists — unless this invocation
64017
+ // was abandoned, in which case the entry may already belong to a
64018
+ // fresher retry and must not be marked with a stale error.
64019
+ if (!invocation.abandoned) {
64020
+ const cached = this.cache.get(key);
64021
+ if (cached) {
64022
+ cached.lastError = error;
64023
+ cached.isRefreshing = false;
64024
+ }
63627
64025
  }
63628
64026
  throw error;
63629
64027
  }
@@ -63655,13 +64053,23 @@ class StampedeProtectedCache {
63655
64053
  });
63656
64054
  }
63657
64055
  /**
63658
- * Apply probabilistic jitter to expiration time
64056
+ * Apply probabilistic jitter to an entry's expiration time.
64057
+ *
64058
+ * The jitter must scale with the ENTRY's own TTL: using `defaultTtl` here
64059
+ * (as this method originally did) mis-anchored `createdAt` for any entry
64060
+ * cached with a custom TTL, swinging its effective expiry by up to
64061
+ * ±(defaultTtl - ttl) — a 50ms entry under a 5s default could randomly
64062
+ * read as already-expired at write time or fresh for 10x its TTL.
64063
+ *
64064
+ * @param originalExpiresAt - The entry's unjittered expiry timestamp (ms).
64065
+ * @param ttl - The TTL (ms) the entry was cached with.
64066
+ * @returns The jittered expiry timestamp.
63659
64067
  */
63660
- applyJitter(originalExpiresAt) {
64068
+ applyJitter(originalExpiresAt, ttl) {
63661
64069
  const range = this.options.maxJitter - this.options.minJitter;
63662
64070
  const jitter = this.options.minJitter + Math.random() * range;
63663
- const createdAt = originalExpiresAt - this.options.defaultTtl;
63664
- const jitteredTtl = this.options.defaultTtl * jitter;
64071
+ const createdAt = originalExpiresAt - ttl;
64072
+ const jitteredTtl = ttl * jitter;
63665
64073
  return createdAt + jitteredTtl;
63666
64074
  }
63667
64075
  /**
@@ -63746,7 +64154,7 @@ const DEFAULT_CACHE_OPTIONS = {
63746
64154
  minJitter: 0.9, // 90%
63747
64155
  maxJitter: 1.1, // 110%
63748
64156
  enableBackgroundRefresh: true,
63749
- loadTimeoutMs: 30000, // 30s hard loader ceiling (anti-pinning)
64157
+ loadTimeoutMs: DEFAULT_LOAD_TIMEOUT_MS, // hard loader ceiling (anti-pinning)
63750
64158
  };
63751
64159
 
63752
64160
  /**
@@ -70628,6 +71036,7 @@ exports.DEFAULT_RISK_FREE_RATE = DEFAULT_RISK_FREE_RATE;
70628
71036
  exports.DEFAULT_TIMEOUTS = DEFAULT_TIMEOUTS;
70629
71037
  exports.DEFAULT_TRADING_POLICY = DEFAULT_TRADING_POLICY;
70630
71038
  exports.DataFormatError = DataFormatError;
71039
+ exports.DuplicateClientOrderIdError = DuplicateClientOrderIdError;
70631
71040
  exports.HttpClientError = HttpClientError;
70632
71041
  exports.HttpServerError = HttpServerError;
70633
71042
  exports.KEEP_ALIVE_DEFAULTS = KEEP_ALIVE_DEFAULTS;