Skip to content

fix(predict): keep live position data in sync across screens#29527

Merged
caieu merged 12 commits into
mainfrom
predict/PRED-820-investigate-ways-to-improve-position-data-consistency-on-home-screen
May 6, 2026
Merged

fix(predict): keep live position data in sync across screens#29527
caieu merged 12 commits into
mainfrom
predict/PRED-820-investigate-ways-to-improve-position-data-consistency-on-home-screen

Conversation

@caieu
Copy link
Copy Markdown
Contributor

@caieu caieu commented Apr 29, 2026

Description

This PR fixes inconsistencies in Predict position values across the home screen, market details, and card/list surfaces.

It moves active-position live updates into usePredictPositions via usePredictLivePositions, syncs websocket-derived values back into the shared React Query positions cache, removes duplicate component-level live subscriptions, scopes live subscriptions to focused screens only, and fixes market websocket unsubscribe behavior so overlapping token subscriptions do not break updates on other active screens. It also updates the positions header to read claimable positions from usePredictPositions instead of Redux/controller state so it stays aligned with the shared query source.

Changelog

CHANGELOG entry: Fixed live prediction position values so they stay updated across the home screen and market details views.

Related issues

Refs: https://consensyssoftware.atlassian.net/browse/PRED-820

Manual testing steps

Feature: live predict positions stay synchronized across screens

  Scenario: home positions continue updating after visiting market details
    Given the user has at least one active Predict position
    And the user is on the Wallet home screen with the Predictions positions section visible
    When the user waits for live position values to update
    And opens one of the active positions into market details
    And waits for live position values to update on market details
    And navigates back to the Wallet home screen
    Then the same home position continues receiving live value updates

  Scenario: only the focused screen keeps market subscriptions active
    Given the user has active Predict positions on the Wallet home screen
    When the user opens a single position in market details
    Then market details only shows live updates for the focused position tokens
    And no unrelated position updates continue streaming from the hidden home screen

  Scenario: claimable positions still render correctly
    Given the user has claimable Predict positions
    When the user opens surfaces that show claimable positions
    Then claimable amounts still render correctly
    And the positions header claim button amount matches the claimable positions list

Screenshots/Recordings

Before

N/A

After

N/A

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Moderate risk because it changes how live price updates propagate through shared React Query caches and alters WebSocket subscription/unsubscribe behavior, which could affect update frequency and data correctness across multiple screens.

Overview
Ensures Predict active position values stay consistent across home, market details, and card surfaces by adding an opt-in livePriceUpdates flag to usePredictPositions that enables usePredictLivePositions to sync websocket-derived PnL/value updates back into the shared positions query cache.

Removes component-level live position mapping (PredictPicks, PredictPicksForCard) in favor of consuming already-updated usePredictPositions data, and scopes live subscriptions to focused screens while skipping claimable positions.

Fixes Polymarket market-price WebSocket unsubscribe logic to avoid unsubscribing token IDs still required by other active subscriptions, and updates PredictPositionsHeader to derive won/claimable positions from usePredictPositions instead of Redux state; tests were updated/added to cover the new live-update and unsubscribe semantics.

Reviewed by Cursor Bugbot for commit 8eda7a8. Bugbot is set up for automated code reviews on this repo. Configure here.

caieu added 3 commits April 29, 2026 09:53
- gate live predict position subscriptions and cache sync by screen focus
- avoid unsubscribing overlapping market tokens still used elsewhere
- move positions header claimable data onto usePredictPositions
- update hook and websocket manager tests for subscription lifecycle
@github-actions
Copy link
Copy Markdown
Contributor

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.

@metamaskbotv2 metamaskbotv2 Bot added the team-predict Predict team label Apr 29, 2026
@caieu caieu marked this pull request as ready for review May 4, 2026 22:17
@caieu caieu requested review from a team as code owners May 4, 2026 22:17
@MarioAslau
Copy link
Copy Markdown
Contributor

Hey @caieu , some things i picked up on

High Severity

1. usePredictLivePositions return value discarded when called from usePredictPositions — hook now has two undifferentiated modes

File: app/components/UI/Predict/hooks/usePredictPositions.ts

usePredictLivePositions(query.data ?? EMPTY_POSITIONS, {
  enabled: enabled && claimable !== true,
  cacheAddress: address,
});

The return value (livePositions, isConnected, lastUpdateTime) is fully discarded. The hook is called purely for the cache-write side effect in its internal useEffect. This creates two undocumented modes of operation:

  • With cacheAddress (called from usePredictPositions): acts as a cache-write side effect; livePositions is computed but never surfaced to any consumer.
  • Without cacheAddress (called directly from a component): returns live-updated positions; no cache sync occurs.

If any component later calls usePredictLivePositions directly (expecting live-priced positions) without passing cacheAddress, it will get updated values for rendering, but those values won't flow back into the shared cache — creating a split-source-of-truth scenario.

Suggestion: Separate the two responsibilities into distinct hooks — e.g., keep usePredictLivePositions for returning live position values to components, and introduce a private useSyncLivePositionsToCache (not exported) inside usePredictPositions.ts for the cache-write effect.


2. Every caller of usePredictPositions now implicitly activates WebSocket subscriptions — invisible side effect

File: app/components/UI/Predict/hooks/usePredictPositions.ts + PredictPositionsHeader.tsx

PredictPositionsHeader now makes two calls:

const { data: activePositions } = usePredictPositions({ claimable: false });
const { data: claimablePositions = [] } = usePredictPositions({ claimable: true });

Each call internally invokes usePredictLivePositions. The claimable: true call correctly passes enabled: false, disabling its live subscription. However, the contract is opaque: any future caller of usePredictPositions will unknowingly activate a WebSocket subscription and a React Query cache-write effect. This makes it harder to reason about what mounts a subscription, and harder to test in isolation.

If two instances of usePredictPositions({ claimable: false }) are simultaneously mounted on focused screens (e.g., in a split-screen scenario, a modal, or a tab navigator with multiple active tabs), both will attempt to write to the same cache key. The useIsFocused guard mitigates this for standard stack navigation, but it is not a structural guarantee.


Medium Severity

3. livePositionUpdates always produces a new Map reference, causing the useEffect to fire one extra time after every price update

File: app/components/UI/Predict/hooks/usePredictLivePositions.ts

const livePositionUpdates = useMemo(() => {
  const updates = new Map<...>();
  livePositions.forEach(...);
  return updates;
}, [livePositions, positions]);

Every recompute of this memo returns a new Map instance. The useEffect depends on livePositionUpdates, so it fires on every new Map reference, even when the Map is empty.

The lifecycle for a single price tick is:

  1. Price update → livePositionUpdates has entries → useEffect fires → setQueryData updates cache.
  2. Cache update → query.data changes → new positions reference passed in → livePositions recomputes (same values, same ref via hasChanges = false guard) → livePositionUpdates recomputes as a new empty Map → useEffect fires again → early-returns on livePositionUpdates.size === 0.

Step 2 is a no-op effect call on every price update. While not a correctness bug, it is unnecessary overhead that scales with update frequency and active position count.

Suggestion: Return the previous Map reference when contents are unchanged, or switch the useEffect dependency to livePositions directly and skip the intermediate Map.


4. PredictPositionsHeader claimable filter may be redundant or silently incorrect

File: app/components/UI/Predict/components/PredictPositionsHeader/PredictPositionsHeader.tsx

const wonPositions = useMemo(
  () =>
    claimablePositions.filter(
      (position) => position.status === PredictPositionStatus.WON,
    ),
  [claimablePositions],
);

usePredictPositions({ claimable: true }) already selects positions where position.claimable === true. If all claimable positions always carry status === WON, the additional filter is dead code and can be removed. If claimable positions can have other statuses (e.g., PENDING_CLAIM), the filter is load-bearing — but this is not documented, and the old selectPredictWonPositions Redux selector should be checked to confirm its exact semantics were preserved.


5. Brief data gap possible during focus transitions

File: app/components/UI/Predict/hooks/usePredictLivePositions.ts

When a screen loses focus, useLiveMarketPrices is called with enabled: false (because enabled && isScreenFocused && tokenIds.length > 0 becomes false). This tears down the WebSocket subscription. When focus returns, the subscription is re-established. Any price update that arrives in the gap between unsubscribe and resubscribe is missed and not recovered. Whether this gap is acceptable depends on how often prices update and how long navigation transitions take, but there is no stale-data recovery mechanism (e.g., a refetch on focus).


Low Severity

6. cacheAddress is @internal on a public exported interface — no TypeScript enforcement

File: app/components/UI/Predict/hooks/usePredictLivePositions.ts

export interface UseLivePositionsOptions {
  enabled?: boolean;
  /**
   * Address-scoped positions cache to sync live values into
   * @internal
   */
  cacheAddress?: string;
}

UseLivePositionsOptions is exported. Any consumer of usePredictLivePositions can pass cacheAddress and unknowingly activate cache-write behavior. TypeScript will not warn them. If cacheAddress is truly internal, consider moving it to a separate unexported options type and overloading or wrapping the function.


7. Integration test for cache sync re-implements sync logic in the mock — tests the mock, not the hook

File: app/components/UI/Predict/hooks/usePredictPositions.test.ts (L406+)

The test at the bottom of the file creates a complex mockUsePredictLivePositions implementation that manually re-creates the setQueryData pattern, including hardcoded query key ['predict', 'positions', options.cacheAddress]. If predictQueries.positions.keys.byAddress() key format changes, this test will silently use the wrong key and continue to pass. The test is essentially testing its own mock, not the real usePredictLivePositions behavior.

Suggestion: Use the real usePredictLivePositions in this test (only mock the WebSocket layer via useLiveMarketPrices) so the cache key and sync logic are covered by the actual implementation.


8. Pure indentation refactor of HomepagePredictPositions adds noise to the diff

File: app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx

The entire HomepagePredictPositions component is re-indented with no logic change. This makes the diff harder to review and obscures the actual semantic change (none). Should be a separate formatting commit or excluded.

@github-actions github-actions Bot added size-XL and removed size-L labels May 5, 2026
Comment thread app/components/UI/Predict/hooks/usePredictLivePositions.ts
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fa804eb. Configure here.

Comment thread app/components/UI/Predict/hooks/usePredictLivePositions.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePredictions, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePredict
  • Risk Level: medium
  • AI Confidence: 90%
click to see 🤖 AI reasoning details

E2E Test Selection:
All 23 changed files are confined to the Predict/Polymarket feature area and its test infrastructure:

  1. Core hook refactor (usePredictLivePositions.ts, usePredictPositions.ts): The live positions hook was refactored from returning transformed data to a side-effect hook that writes directly into the React Query cache. This is a significant architectural change in how live price updates flow through the Predict feature. The new livePriceUpdates option in usePredictPositions integrates this internally.

  2. WebSocket fix (WebSocketManager.ts): Bug fix for unsubscribe logic - now correctly filters out token IDs that are still subscribed elsewhere before sending unsubscribe messages. This prevents premature disconnection when multiple components share subscriptions.

  3. Component updates (PredictPicks, PredictPicksForCard, PredictSportCardFooter, PredictHomePositions, PredictGameDetailsContent, PredictMarketDetails): All updated to use livePriceUpdates: true option instead of directly calling usePredictLivePositions. Components now render from positions (cache-synced) instead of livePositions.

  4. PredictPositionsHeader: Refactored won positions logic from Redux selector to usePredictPositions with claimable: true.

  5. Homepage integration (usePredictPositionsForHomepage.ts): Added live price updates for active positions on the homepage Predictions section.

  6. Test infrastructure (tests/component-view/mocks.ts): Added mocks for subscribeToMarketPrices and getConnectionStatus - critical file change but scoped to Predict feature support.

Tag selection rationale:

  • SmokePredictions: Primary tag - all changes are in the Predict/Polymarket feature. The live price update refactor, WebSocket fix, and component changes directly affect position display, cash-out flows, and balance verification.
  • SmokeWalletPlatform: Required per SmokePredictions description - Predictions is a section inside the Trending tab, and the homepage Predictions section changes (usePredictPositionsForHomepage.ts, PredictionsSection.test.tsx) directly affect the Trending/Homepage integration.
  • SmokeConfirmations: Required per SmokePredictions description - opening/closing positions are on-chain transactions that go through the confirmations flow.

No other tags are needed as changes are fully contained within the Predict feature with no impact on navigation, shared components, Engine, or other feature areas.

Performance Test Selection:
The refactored live price update mechanism now writes directly into the React Query cache via useEffect on every price update from WebSocket. This cache-write pattern on live data could impact rendering performance for the Predict market details and positions views. The @PerformancePredict tag covers prediction market list loading, market details, deposit flows, and balance display - all of which are affected by this architectural change to how live prices are propagated.

View GitHub Actions results

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

@MarioAslau MarioAslau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ! Thanks for addressing the review

@caieu caieu added this pull request to the merge queue May 6, 2026
Merged via the queue into main with commit 6b17dc5 May 6, 2026
99 of 100 checks passed
@caieu caieu deleted the predict/PRED-820-investigate-ways-to-improve-position-data-consistency-on-home-screen branch May 6, 2026 12:35
@github-actions github-actions Bot locked and limited conversation to collaborators May 6, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.77.0 Issue or pull request that will be included in release 7.77.0 label May 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.77.0 Issue or pull request that will be included in release 7.77.0 size-XL team-predict Predict team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants