@adaptic/utils 0.0.991 → 0.0.992
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 +85 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +85 -0
- package/dist/index.mjs.map +1 -1
- package/dist/test.js +85 -0
- package/dist/test.js.map +1 -1
- package/dist/types/alpaca-market-data-api.d.ts +42 -0
- package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2691,6 +2691,48 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
2691
2691
|
};
|
|
2692
2692
|
reconnectAttempts = {};
|
|
2693
2693
|
reconnectTimers = {};
|
|
2694
|
+
/**
|
|
2695
|
+
* Wall-clock timestamp of the most recent Alpaca app-level error code
|
|
2696
|
+
* 406 ("connection limit exceeded") received on each stream. Used by
|
|
2697
|
+
* {@link scheduleReconnect} to apply a long backoff with jitter rather
|
|
2698
|
+
* than the normal sub-second exponential ramp — without this, hitting
|
|
2699
|
+
* Alpaca's account-wide concurrent-connection cap (typical for blue/green
|
|
2700
|
+
* deploy rollovers where the old pod's WS slots haven't been released
|
|
2701
|
+
* yet) produced a 10-attempt retry storm that compounded the slot
|
|
2702
|
+
* pressure and consumed the per-account connection quota across the
|
|
2703
|
+
* organisation.
|
|
2704
|
+
*
|
|
2705
|
+
* Cleared once the long-backoff retry is scheduled so that subsequent
|
|
2706
|
+
* normal failures fall back to the standard sub-second exponential.
|
|
2707
|
+
*
|
|
2708
|
+
* @see CONNECTION_LIMIT_BACKOFF_MS / CONNECTION_LIMIT_BACKOFF_JITTER_MS
|
|
2709
|
+
*/
|
|
2710
|
+
lastConnectionLimitAt = {};
|
|
2711
|
+
/**
|
|
2712
|
+
* Five-minute base backoff after Alpaca's app-level 406. Long enough
|
|
2713
|
+
* for Alpaca's server-side cleanup to release stale slots in typical
|
|
2714
|
+
* rollover scenarios; short enough that an operator doesn't need to
|
|
2715
|
+
* intervene. Mirrors the equivalent MassiveClient
|
|
2716
|
+
* `MAX_CONNECTIONS_RETRY_DELAY_MS` (engine v1.0.59) so both providers
|
|
2717
|
+
* behave identically under the same failure mode.
|
|
2718
|
+
*/
|
|
2719
|
+
CONNECTION_LIMIT_BACKOFF_MS = 5 * 60_000;
|
|
2720
|
+
/**
|
|
2721
|
+
* ±30 s of uniform jitter on the connection-limit backoff. Prevents
|
|
2722
|
+
* a thundering-herd retry when all three streams (stock / option /
|
|
2723
|
+
* crypto) hit 406 simultaneously during a deploy rollover — without
|
|
2724
|
+
* jitter they'd all retry at the same wall-clock instant and could
|
|
2725
|
+
* re-trip the account cap together.
|
|
2726
|
+
*/
|
|
2727
|
+
CONNECTION_LIMIT_BACKOFF_JITTER_MS = 30_000;
|
|
2728
|
+
/**
|
|
2729
|
+
* Recency window within which a 406 is considered "still applicable"
|
|
2730
|
+
* to a subsequent reconnect-schedule call. The 406 message handler
|
|
2731
|
+
* stamps {@link lastConnectionLimitAt} and the `close` handler fires
|
|
2732
|
+
* shortly afterwards (sub-second typically) — the window is wide
|
|
2733
|
+
* enough to absorb scheduling delays without false-positives.
|
|
2734
|
+
*/
|
|
2735
|
+
CONNECTION_LIMIT_RECENCY_MS = 30_000;
|
|
2694
2736
|
setMode(mode = "production") {
|
|
2695
2737
|
if (mode === "sandbox") {
|
|
2696
2738
|
// sandbox mode
|
|
@@ -2822,6 +2864,18 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
2822
2864
|
}
|
|
2823
2865
|
else if (message.T === "error") {
|
|
2824
2866
|
log$l(`${streamType} stream error: ${message.msg} (code: ${message.code}, raw: ${JSON.stringify(message)})`, { type: "error" });
|
|
2867
|
+
// Alpaca code 406: "connection limit exceeded" — account-wide
|
|
2868
|
+
// concurrent-WS cap reached. The Alpaca server will close the
|
|
2869
|
+
// socket immediately after this frame, which would normally
|
|
2870
|
+
// trigger our standard sub-second exponential reconnect chain
|
|
2871
|
+
// (1 s, 2 s, 4 s, 8 s, 16 s, 30 s × 5) — exactly the wrong
|
|
2872
|
+
// behaviour against a rate-limit response. Stamp the recency
|
|
2873
|
+
// marker so {@link scheduleReconnect} switches to the
|
|
2874
|
+
// 5-minute jittered backoff instead.
|
|
2875
|
+
if (typeof message.code === "number" &&
|
|
2876
|
+
message.code === 406) {
|
|
2877
|
+
this.lastConnectionLimitAt[streamType] = Date.now();
|
|
2878
|
+
}
|
|
2825
2879
|
}
|
|
2826
2880
|
else if (message.S) {
|
|
2827
2881
|
super.emit(`${streamType}-${message.T}`, message);
|
|
@@ -2855,6 +2909,37 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
2855
2909
|
});
|
|
2856
2910
|
}
|
|
2857
2911
|
scheduleReconnect(streamType) {
|
|
2912
|
+
// 406-recovery fast path. When the most recent close was preceded
|
|
2913
|
+
// by an Alpaca app-level 406 ("connection limit exceeded"), the
|
|
2914
|
+
// standard sub-second exponential ramp is exactly wrong — it
|
|
2915
|
+
// hammers the rate-limit endpoint and prolongs the slot pressure.
|
|
2916
|
+
// Use a 5-minute jittered backoff instead and reset the normal
|
|
2917
|
+
// attempt counter so we don't fall off the end of maxAttempts
|
|
2918
|
+
// and permanently give up on a transient rollover blip.
|
|
2919
|
+
const connectionLimitAt = this.lastConnectionLimitAt[streamType];
|
|
2920
|
+
const isRecentConnectionLimit = typeof connectionLimitAt === "number" &&
|
|
2921
|
+
Date.now() - connectionLimitAt <= this.CONNECTION_LIMIT_RECENCY_MS;
|
|
2922
|
+
if (isRecentConnectionLimit) {
|
|
2923
|
+
const jitter = Math.floor((Math.random() - 0.5) *
|
|
2924
|
+
2 *
|
|
2925
|
+
this.CONNECTION_LIMIT_BACKOFF_JITTER_MS);
|
|
2926
|
+
const delayMs = this.CONNECTION_LIMIT_BACKOFF_MS + jitter;
|
|
2927
|
+
// Reset normal attempt counter so the next 406 retry doesn't
|
|
2928
|
+
// inherit a stale exponential cap.
|
|
2929
|
+
this.reconnectAttempts[streamType] = 0;
|
|
2930
|
+
// Consume the recency marker — subsequent reconnects fall back
|
|
2931
|
+
// to the standard exponential path unless a new 406 arrives.
|
|
2932
|
+
delete this.lastConnectionLimitAt[streamType];
|
|
2933
|
+
log$l(`${streamType} stream: Alpaca 406 connection-limit recovery — backing off ${Math.round(delayMs / 1000)}s before retry to allow account-wide slot release`, { type: "warn" });
|
|
2934
|
+
if (this.reconnectTimers[streamType]) {
|
|
2935
|
+
clearTimeout(this.reconnectTimers[streamType]);
|
|
2936
|
+
}
|
|
2937
|
+
this.reconnectTimers[streamType] = setTimeout(() => {
|
|
2938
|
+
log$l(`${streamType} stream: attempting reconnect after 406-recovery backoff`, { type: "info" });
|
|
2939
|
+
this.connect(streamType);
|
|
2940
|
+
}, delayMs);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2858
2943
|
const attempts = this.reconnectAttempts[streamType] ?? 0;
|
|
2859
2944
|
const maxAttempts = 10;
|
|
2860
2945
|
if (attempts >= maxAttempts) {
|