From 2745ff92ccb6dd0947f8f19ecd43c19d57a34d33 Mon Sep 17 00:00:00 2001 From: Steve Freeman Date: Fri, 11 Sep 2026 16:58:50 -0400 Subject: [PATCH 1/2] fix(fantasy): stop marketValue from returning null on cold cache getFantasyCalcValuesForOverview waited only 150ms before falling back to an empty values map, but the underlying FantasyCalc request can take up to FANTASYCALC_TIMEOUT_MS (2000ms). On a cold in-memory cache (e.g. right after a deploy), the overview almost always timed out before the fetch completed, so every player's marketValue and positionRank came back null until a second request warmed the cache. Align the overview wait with the request timeout so the first request has a real chance to populate market values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/backend/src/fantasy/fantasy.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/fantasy/fantasy.service.ts b/packages/backend/src/fantasy/fantasy.service.ts index 34aad527..f250f13a 100644 --- a/packages/backend/src/fantasy/fantasy.service.ts +++ b/packages/backend/src/fantasy/fantasy.service.ts @@ -48,7 +48,10 @@ const WAIVER_MARKET_CACHE_MS = 6 * 60 * 60 * 1000; const FANTASYCALC_CACHE_MS = 6 * 60 * 60 * 1000; const FANTASYCALC_FAILURE_CACHE_MS = 5 * 60 * 1000; const FANTASYCALC_TIMEOUT_MS = 2000; -const FANTASYCALC_OVERVIEW_WAIT_MS = 150; +// Must be long enough for the FantasyCalc request (bounded by FANTASYCALC_TIMEOUT_MS) to actually +// finish on a cold cache; a short wait here mostly guarantees the fallback (all-null marketValues) +// on every first request after a deploy, since the in-memory cache resets on restart. +const FANTASYCALC_OVERVIEW_WAIT_MS = FANTASYCALC_TIMEOUT_MS; const WAIVER_HISTORY_SEASONS = 3; const NFL_REGULAR_SEASON_WEEKS = 18; const SLEEPER_ID_PATTERN = /^\d{1,32}$/; From 62a9879c843c50bf452d84b46e68beb9304f1028 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:09:14 +0000 Subject: [PATCH 2/2] test(fantasy): cover delayed cold-cache overview values --- .../src/fantasy/fantasy.service.spec.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/backend/src/fantasy/fantasy.service.spec.ts b/packages/backend/src/fantasy/fantasy.service.spec.ts index b904e742..131062d5 100644 --- a/packages/backend/src/fantasy/fantasy.service.spec.ts +++ b/packages/backend/src/fantasy/fantasy.service.spec.ts @@ -1702,6 +1702,66 @@ describe('FantasyService', () => { } }); + it('waits for delayed cold-cache FantasyCalc responses before falling back', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-09-11T12:00:00.000Z')); + const internals = service as unknown as FantasyServiceInternals; + const league: SleeperLeague = { + league_id: '998', + name: 'Redraft League', + season: '2026', + status: 'in_season', + avatar: null, + total_rosters: 12, + roster_positions: ['QB'], + }; + (Axios.get as Mock).mockImplementationOnce( + () => + new Promise<{ data: FantasyCalcResponseEntry[] }>((resolve) => { + setTimeout( + () => + resolve({ + data: [ + { + player: { sleeperId: 'p2' }, + value: 1234, + redraftValue: 1234, + overallRank: 50, + positionRank: 20, + trend30Day: 1, + }, + ], + }), + 300, + ); + }), + ); + + try { + const snapshotPromise = internals.getFantasyCalcValuesForOverview(league); + await vi.advanceTimersByTimeAsync(300); + + await expect(snapshotPromise).resolves.toEqual({ + values: new Map([ + [ + 'p2', + { + sleeperId: 'p2', + value: 1234, + overallRank: 50, + positionRank: 20, + trend30Day: 1, + tradeFrequency: null, + }, + ], + ]), + version: 1, + }); + } finally { + vi.useRealTimers(); + } + }); + it('keeps the overview FantasyCalc version aligned with the fallback values snapshot', async () => { const internals = service as unknown as FantasyServiceInternals; const serviceState = service as unknown as {