Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d5d15a1
feat: Investigate residual rate-limit (429) issues on Extension Perps
abretonc7s Apr 18, 2026
e8b5125
fix(perps): coalesce candle subscribe races to cut HL 429s (TAT-2986)
abretonc7s Apr 18, 2026
4847c2d
fix(perps): reset candle debounce state on disconnect/reconnect
abretonc7s Apr 19, 2026
70693c8
fix(perps): force candlestick full-setData on symbol/interval change
abretonc7s Apr 19, 2026
42db5f0
fix(perps): coalesce activity-page HL calls; show skeleton on first load
abretonc7s Apr 19, 2026
a5b20e3
fix(perps): clear pending timer on reconnect; invalidate coalesce cac…
abretonc7s Apr 19, 2026
ddb0cf9
fix(perps): cache-respecting initial mount for activity-page fetches
abretonc7s Apr 20, 2026
df320e5
refactor(perps): drop void 'ok' return and name candle-teardown delay
abretonc7s Apr 20, 2026
beca114
fix(perps): scope history coalesce keys + drop redundant activity-pag…
abretonc7s Apr 20, 2026
dcbb28c
fix(perps): force fresh transaction history on Activity page mount
abretonc7s Apr 20, 2026
5d32f4b
fix(perps): clean up inFlight entry when coalesced fn throws synchron…
abretonc7s Apr 20, 2026
eec9457
fix(perps): refetch activity on scope change; invalidate in-flight co…
abretonc7s Apr 20, 2026
5b27d2c
fix(perps): coalesce concurrent candle activations across pending init
abretonc7s Apr 20, 2026
84c71ac
fix(perps): guard candle activation against destroy; dedup coalesce keys
abretonc7s Apr 20, 2026
8e651aa
refactor(perps): drop unused 'ok' return from activate bridge methods
abretonc7s Apr 20, 2026
40cafb4
fix(perps): unfreeze candles after reconnect; preserve remount debounce
abretonc7s Apr 20, 2026
9cccbd2
fix(perps): debounce candle resubscribe from last disconnect, not las…
abretonc7s Apr 20, 2026
b40700e
fix(perps): preserve in-flight coalesce; guard hooks against scope races
abretonc7s Apr 20, 2026
09ea79a
fix(perps): drop coalesce cache on scope teardown (lock/signout/accou…
abretonc7s Apr 20, 2026
f851e9d
fix(perps): wait only the remaining debounce budget on partial-window…
abretonc7s Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 185 additions & 13 deletions app/scripts/controllers/perps/perps-stream-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ describe('PerpsStreamBridge', () => {
(
api.perpsActivateStreaming as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)(params);

it('activates static subscriptions if not yet activated', async () => {
Expand Down Expand Up @@ -593,7 +593,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivatePriceStream as (p: {
symbols: string[];
}) => Promise<string>
}) => Promise<void>
)({
symbols: ['ETH', 'BTC'],
});
Expand All @@ -620,7 +620,7 @@ describe('PerpsStreamBridge', () => {
api.perpsActivatePriceStream as (p: {
symbols: string[];
includeMarketData?: boolean;
}) => Promise<string>
}) => Promise<void>
)({
symbols: ['ETH'],
includeMarketData: true,
Expand All @@ -645,7 +645,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivatePriceStream as (p: {
symbols: string[];
}) => Promise<string>
}) => Promise<void>
)({
symbols: ['ETH'],
});
Expand Down Expand Up @@ -674,7 +674,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateOrderBookStream as (p: {
symbol: string;
}) => Promise<string>
}) => Promise<void>
)({
symbol: 'ETH',
});
Expand All @@ -701,7 +701,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateOrderBookStream as (p: {
symbol: string;
}) => Promise<string>
}) => Promise<void>
)({
symbol: 'ETH',
});
Expand All @@ -722,7 +722,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({ symbol: 'ETH', interval: '1h', duration: '1d' });

expect(controller.subscribeToCandles).toHaveBeenCalledWith({
Expand All @@ -742,6 +742,7 @@ describe('PerpsStreamBridge', () => {
});

it('deactivateCandleStream tears down candle subscription', async () => {
jest.useFakeTimers();
const controller = createMockController();
const unsub = jest.fn();
controller.subscribeToCandles.mockReturnValue(unsub);
Expand All @@ -753,7 +754,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({ symbol: 'ETH', interval: '1h' });
(
api.perpsDeactivateCandleStream as (p: {
Expand All @@ -762,10 +763,14 @@ describe('PerpsStreamBridge', () => {
}) => void
)({ symbol: 'ETH', interval: '1h' });

jest.advanceTimersByTime(150);

expect(unsub).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});

it('keeps other candle streams when deactivating one symbol+interval', async () => {
jest.useFakeTimers();
const controller = createMockController();
const unsubBtc = jest.fn();
const unsubEth = jest.fn();
Expand All @@ -784,20 +789,187 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({ symbol: 'BTC', interval: '1h' });
await (
api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({ symbol: 'ETH', interval: '4h' });

expect(controller.subscribeToCandles).toHaveBeenCalledTimes(2);

deactivate({ symbol: 'BTC', interval: '1h' });

jest.advanceTimersByTime(150);

expect(unsubBtc).toHaveBeenCalledTimes(1);
expect(unsubEth).not.toHaveBeenCalled();
jest.useRealTimers();
});

it('short-circuits when the same candle stream is already active', async () => {
jest.useFakeTimers();
const controller = createMockController();
const { bridge } = createBridge({
controller: controller as unknown as PerpsController,
});
const api = bridge.bridgeApi();
const activate = api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<void>;

await activate({ symbol: 'BTC', interval: '5m' });
await activate({ symbol: 'BTC', interval: '5m' });

expect(controller.subscribeToCandles).toHaveBeenCalledTimes(1);

jest.useRealTimers();
});

it('does not subscribe when destroy() runs while init is pending', async () => {
const controller = createMockController();
let resolveInit: (() => void) | undefined;
const controllerApi = createMockControllerApi();
controllerApi.perpsInit.mockImplementation(
() =>
new Promise<void>((resolve) => {
resolveInit = resolve;
}),
);
const { bridge } = createBridge({
controller: controller as unknown as PerpsController,
controllerApi,
});
const api = bridge.bridgeApi();
const activate = api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<void>;

const pending = activate({ symbol: 'BTC', interval: '5m' });
// Let the activation queue behind init, then tear the bridge down.
await Promise.resolve();
bridge.destroy();

resolveInit?.();
await pending;

expect(controller.subscribeToCandles).not.toHaveBeenCalled();
});

it('coalesces concurrent activate calls for the same key across pending init', async () => {
const controller = createMockController();
let resolveInit: (() => void) | undefined;
const controllerApi = createMockControllerApi();
controllerApi.perpsInit.mockImplementation(
() =>
new Promise<void>((resolve) => {
resolveInit = resolve;
}),
);
const { bridge } = createBridge({
controller: controller as unknown as PerpsController,
controllerApi,
});
const api = bridge.bridgeApi();
const activate = api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<void>;

const first = activate({ symbol: 'BTC', interval: '5m' });
const second = activate({ symbol: 'BTC', interval: '5m' });
const third = activate({ symbol: 'BTC', interval: '5m' });

// Let both callers pass the synchronous guards and queue behind init.
await Promise.resolve();

resolveInit?.();
await Promise.all([first, second, third]);

expect(controller.subscribeToCandles).toHaveBeenCalledTimes(1);
expect(controllerApi.perpsInit).toHaveBeenCalledTimes(1);
});

it('resubscribes after destroy() when a later activate runs', async () => {
const controller = createMockController();
const { bridge } = createBridge({
controller: controller as unknown as PerpsController,
});
const api = bridge.bridgeApi();
const activate = api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<void>;

await activate({ symbol: 'BTC', interval: '5m' });
expect(controller.subscribeToCandles).toHaveBeenCalledTimes(1);

// Simulate perpsDisconnect / perpsToggleTestnet tearing the bridge down.
bridge.destroy();

// A later activate (e.g. after user reconnects or flips testnet) must
// issue a fresh subscribe, not be permanently suppressed by a latched
// destroyed flag.
await activate({ symbol: 'BTC', interval: '5m' });
expect(controller.subscribeToCandles).toHaveBeenCalledTimes(2);
});

it('cancels deferred teardown when matching activate arrives within 150ms', async () => {
jest.useFakeTimers();
const controller = createMockController();
const unsub = jest.fn();
controller.subscribeToCandles.mockReturnValue(unsub);
const { bridge } = createBridge({
controller: controller as unknown as PerpsController,
});
const api = bridge.bridgeApi();
const activate = api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<void>;
const deactivate = api.perpsDeactivateCandleStream as (p: {
symbol: string;
interval: string;
}) => void;

await activate({ symbol: 'BTC', interval: '5m' });
deactivate({ symbol: 'BTC', interval: '5m' });

await activate({ symbol: 'BTC', interval: '5m' });

jest.advanceTimersByTime(200);

expect(unsub).not.toHaveBeenCalled();
expect(controller.subscribeToCandles).toHaveBeenCalledTimes(1);

jest.useRealTimers();
});

it('clears pending candle teardown timers on destroy', async () => {
jest.useFakeTimers();
const controller = createMockController();
const unsub = jest.fn();
controller.subscribeToCandles.mockReturnValue(unsub);
const { bridge } = createBridge({
controller: controller as unknown as PerpsController,
});
const api = bridge.bridgeApi();
const activate = api.perpsActivateCandleStream as (
p: Record<string, unknown>,
) => Promise<void>;
const deactivate = api.perpsDeactivateCandleStream as (p: {
symbol: string;
interval: string;
}) => void;

await activate({ symbol: 'BTC', interval: '5m' });
deactivate({ symbol: 'BTC', interval: '5m' });

bridge.destroy();

jest.advanceTimersByTime(200);

expect(unsub).toHaveBeenCalledTimes(1);

jest.useRealTimers();
});
});

Expand Down Expand Up @@ -848,7 +1020,7 @@ describe('PerpsStreamBridge', () => {
await (
bridge.bridgeApi().perpsActivateStreaming as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({
priceSymbols: ['ETH'],
orderBookSymbol: 'BTC',
Expand Down Expand Up @@ -878,7 +1050,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateStreaming as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({ priceSymbols: ['ETH'] });

expect(() => bridge.destroy()).not.toThrow();
Expand Down Expand Up @@ -922,7 +1094,7 @@ describe('PerpsStreamBridge', () => {
await (
api.perpsActivateStreaming as (
p: Record<string, unknown>,
) => Promise<string>
) => Promise<void>
)({ priceSymbols: ['ETH'] });

expect(() => {
Expand Down
Loading
Loading