feat(perps): always on socket connection#27103
Conversation
…ion lifecycle
Replace per-section PerpsConnectionProvider connect/disconnect calls with a
single top-level PerpsAlwaysOnProvider mounted at the wallet root.
Previously, multiple PerpsConnectionProvider instances (Homepage, PerpsTabView,
ActivityView, TrendingView, ExploreSearchScreen, UrlAutocomplete) each called
connect()/disconnect(), creating reference-count races that caused intermittent
bugs — positions not showing, 24h values missing — after long app backgrounding.
PerpsAlwaysOnProvider is the sole caller of PerpsConnectionManager.connect() and
disconnect(). It manages AppState transitions (background → disconnect, foreground
→ reconnect with ReconnectionDelayAndroidMs stabilisation) and cleans up on
unmount. connectionRefCount in PerpsConnectionManager stays exactly 1 throughout
the app lifetime.
All PerpsConnectionProvider instances now use manageLifecycle={false}, acting as
React context sources only. The visibility callback plumbing in PerpsTabView and
WalletTokensTabView is also removed as it is no longer needed.
Updated docs/perps to reflect the new architecture.
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
…y tests - Add PerpsAlwaysOnProvider.test.tsx covering connect/disconnect lifecycle, AppState foreground/background handling, timer cancellation, and unmount cleanup - Update Wallet/index.test.tsx to reflect removal of isVisible/onVisibilityChange props from PerpsTabView (lifecycle now owned by PerpsAlwaysOnProvider) New code coverage: 81% on changed lines
…apper
The PerpsConnectionProvider was removed from the perps SectionWrapper in
sections.config.tsx, but useSectionData still calls useContext(PerpsConnectionContext).
Without an ancestor provider the context is null, making error-state handling
dead code (connection failures silently show stale data instead of returning
empty/loading=false).
Restores PerpsConnectionProvider with manageLifecycle={false} so the context
is available while lifecycle remains owned by PerpsAlwaysOnProvider.
Also fixes low-severity bugbot findings in tests:
- Replace toBeTruthy() with toBeOnTheScreen() in PerpsAlwaysOnProvider.test.tsx
- Remove 'should' prefix from renamed test names in Wallet/index.test.tsx
…d UrlAutocomplete
useSectionsData (called by useExploreSearch → ExploreSearchResults) invokes
SECTIONS_CONFIG.perps.useSectionData which reads useContext(PerpsConnectionContext).
ExploreSearchScreen and UrlAutocomplete both render ExploreSearchResults but had
their PerpsConnectionProvider removed, leaving the context null and making
perps error-state handling dead code.
Restores PerpsConnectionProvider with suppressErrorView and manageLifecycle={false}
(context-only, lifecycle stays with PerpsAlwaysOnProvider).
PerpsAlwaysOnProvider was wrapping ErrorBoundary, so a synchronous render error in its useSelector call would bypass the wallet error boundary and crash the app. Swapping the nesting order ensures any render error in PerpsAlwaysOnProvider is caught by the wallet boundary, consistent with the stated goal that a perps failure cannot block the rest of the app.
…nProvider
Every callsite already passes manageLifecycle={false} explicitly. Defaulting
to true was a footgun — any future PerpsConnectionProvider added without the
prop would silently reintroduce the reference-count race conditions this PR
was created to fix.
Update two existing tests that test the manageLifecycle=true path to pass
the prop explicitly now that the default is false.
…onnectionLifecycle hook
With PerpsAlwaysOnProvider owning the entire connection lifecycle, the
manageLifecycle and isVisible props on PerpsConnectionProvider and the
underlying usePerpsConnectionLifecycle hook are dead code.
- Remove manageLifecycle and isVisible props from PerpsConnectionProvider
- Remove usePerpsConnectionLifecycle import and call from PerpsConnectionProvider
- Delete usePerpsConnectionLifecycle hook and its test file
- Remove manageLifecycle={false} from all callsites (no longer needed)
- Update PerpsConnectionProvider tests to remove lifecycle mock and
rewrite lifecycle-dependent tests to use polling-based state instead
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
…overage - Mock AppState.currentState as 'active' in beforeEach so lastAppState initializes correctly in tests (Jest has null, real app has 'active') - Add iOS active→inactive→background test: verifies only one disconnect fires (the inactive transition), not two - Add notification center pull-down test: verifies active→inactive→active disconnects once then reconnects once - Remove accidentally committed TASK_PERPSALWAYSON.md
| it('calls disconnect when app goes to background', () => { | ||
| render( | ||
| <PerpsAlwaysOnProvider> | ||
| <Text>child</Text> | ||
| </PerpsAlwaysOnProvider>, | ||
| ); | ||
|
|
||
| act(() => { | ||
| mockAppStateListener?.('background'); | ||
| }); | ||
|
|
||
| expect(mockDisconnect).toHaveBeenCalledTimes(1); | ||
| }); |
There was a problem hiding this comment.
does it disconnect immediately? or do we still have a graze period
There was a problem hiding this comment.
it calls the disconnect but then htere is grace period until it actually disconnect when it goes ot background
| expect(mockDisconnect).toHaveBeenCalledTimes(1); | ||
| expect(mockSubscriptionRemove).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Do we have a case for when the app locks/unlocks?
There was a problem hiding this comment.
this go via the same path as backgrounded when locked
| {/* Only mount providers when tab is active to prevent polling when hidden */} | ||
| {isPerpsTabActive ? ( | ||
| <PerpsConnectionProvider isVisible={isPerpsTabActive}> | ||
| <PerpsConnectionProvider> |
There was a problem hiding this comment.
if thi sis going to be always on, isn't it better to mount it at the app level?
There was a problem hiding this comment.
I think the main issue is to access redux and load when wallet unlocks. We could potentially go a bit higher but I don't think it would make significant changes, also better to happen after onboarding etc.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: Key changes:
The changes affect:
The risk is medium because:
Performance Test Selection: |
The committed fixture schema is out of date. To update, comment: |
|
|
|
||
| - Manage actual connection lifecycle (delegates to Manager) | ||
| - Know about WebSockets or providers | ||
| - Manage connection lifecycle when `manageLifecycle={false}` (the default for all section-level instances) |
There was a problem hiding this comment.
Where is the manageLifecycle prop defined? Couldn't find it in the code
aganglada
left a comment
There was a problem hiding this comment.
Tested it locally and LGTM



Description
Replaces per-section `PerpsConnectionProvider` connect/disconnect calls with a single top-level `PerpsAlwaysOnProvider` mounted at the wallet root.
Problem: Multiple `PerpsConnectionProvider` instances (Homepage, PerpsTabView, ActivityView, TrendingView, ExploreSearchScreen, UrlAutocomplete) each called `connect()`/`disconnect()`, creating reference-count races in `PerpsConnectionManager`. This caused intermittent bugs — positions not showing, 24h values missing — after long app backgrounding sessions.
Solution: `PerpsAlwaysOnProvider` is the sole caller of `PerpsConnectionManager.connect()` and `disconnect()`. `PerpsConnectionProvider` is now a pure React context source — it polls singleton state and exposes it via `PerpsConnectionContext`, but never calls connect/disconnect itself.
Key changes:
Changelog
CHANGELOG entry: null
Related issues
Fixes:
Manual testing steps
```gherkin
Feature: Perps always-on connection
Scenario: user backgrounds and foregrounds the app
Given perps feature flag is enabled
And user is on the Wallet screen
Scenario: user switches between tabs
Given perps feature flag is enabled
Scenario: user switches accounts
Given perps feature flag is enabled
```
Screenshots/Recordings
Before
Multiple providers each managing lifecycle → reference-count races on background/foreground.
After
Single `PerpsAlwaysOnProvider` owns the lifecycle — `connectionRefCount` stays exactly 1. `PerpsConnectionProvider` is a context-only provider with no lifecycle responsibilities.
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Medium Risk
Changes Perps WebSocket connection ownership from many screen-scoped providers to a single wallet-root lifecycle manager, which can impact reconnect behavior, resource usage, and perps data availability across the app.
Overview
Centralizes Perps WebSocket lifecycle management by introducing
PerpsAlwaysOnProvider(mounted at theWalletroot) as the only component that callsPerpsConnectionManager.connect()/disconnect(), driven byAppStatebackground/foreground transitions with a delayed reconnect and error logging.PerpsConnectionProvideris simplified to a context/state wrapper only: it no longer takesisVisible, no longer unmounts children when hidden, and removes theusePerpsConnectionLifecyclehook entirely (hook + tests deleted, barrel export removed). Call sites (Perps tab, Activity perps transactions tab, homepage Perps section, Explore search, UrlAutocomplete, Trending sections) are updated to stop passing visibility props and to usesuppressErrorViewwhere needed.Tests and docs are updated to match the new always-on architecture, including new coverage for
PerpsAlwaysOnProviderand adjustedPerpsConnectionProviderretry/error behavior.Written by Cursor Bugbot for commit 1644485. This will update automatically on new commits. Configure here.