Skip to content

Commit 8f60c18

Browse files
fix(app): tame core state and rewards timeout noise (tinyhumansai#1822)
Co-authored-by: Rajvardhan Patil <243567420+RajvardhanPatil07@users.noreply.github.com>
1 parent acdc818 commit 8f60c18

4 files changed

Lines changed: 62 additions & 7 deletions

File tree

app/src/providers/CoreStateProvider.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const log = debugFactory('core-state');
4444

4545
const POLL_MS = 2000;
4646
const MAX_BOOTSTRAP_RETRIES = 5;
47+
const SUPPRESS_POLL_WARNING_AT = MAX_BOOTSTRAP_RETRIES + 1;
4748

4849
/** Extract only non-sensitive fields from an RPC/fetch error. */
4950
function sanitizeError(error: unknown): { message?: string; code?: string; status?: number } {
@@ -61,6 +62,19 @@ function sanitizeError(error: unknown): { message?: string; code?: string; statu
6162
return { message: String(error) };
6263
}
6364

65+
export function coreStatePollFailureWarningMessage(failureCount: number): string | null {
66+
if (failureCount <= 0) {
67+
return null;
68+
}
69+
if (failureCount <= MAX_BOOTSTRAP_RETRIES) {
70+
return `[core-state] poll failed (attempt ${failureCount}/${MAX_BOOTSTRAP_RETRIES}):`;
71+
}
72+
if (failureCount === SUPPRESS_POLL_WARNING_AT) {
73+
return '[core-state] poll failed repeatedly; suppressing further warnings until core state recovers:';
74+
}
75+
return null;
76+
}
77+
6478
interface CoreStateContextValue extends CoreState {
6579
refresh: () => Promise<void>;
6680
refreshTeams: () => Promise<void>;
@@ -375,10 +389,10 @@ export default function CoreStateProvider({ children }: { children: ReactNode })
375389
MAX_BOOTSTRAP_RETRIES,
376390
safe
377391
);
378-
console.warn(
379-
`[core-state] poll failed (attempt ${bootstrapFailCountRef.current}/${MAX_BOOTSTRAP_RETRIES}):`,
380-
safe
381-
);
392+
const warningMessage = coreStatePollFailureWarningMessage(bootstrapFailCountRef.current);
393+
if (warningMessage) {
394+
console.warn(warningMessage, safe);
395+
}
382396
if (bootstrapFailCountRef.current >= MAX_BOOTSTRAP_RETRIES) {
383397
commitState(previous => {
384398
if (previous.isBootstrapping) {

app/src/providers/__tests__/CoreStateProvider.test.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
55
import * as coreStateApi from '../../services/coreStateApi';
66
import * as tauriCommands from '../../utils/tauriCommands';
77
import { setCoreStateSnapshot } from '../../lib/coreState/store';
8-
import CoreStateProvider, { useCoreState } from '../CoreStateProvider';
8+
import CoreStateProvider, {
9+
coreStatePollFailureWarningMessage,
10+
useCoreState,
11+
} from '../CoreStateProvider';
912

1013
vi.mock('../../services/coreStateApi');
1114
vi.mock('../../services/analytics', () => ({ syncAnalyticsConsent: vi.fn() }));
@@ -216,6 +219,28 @@ describe('CoreStateProvider — identity-change cache clearing', () => {
216219
await waitFor(() => expect(screen.getByTestId('ready').textContent).toBe('ready'));
217220
});
218221

222+
it('warns when the initial core state poll fails', async () => {
223+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
224+
225+
try {
226+
fetchSnapshot.mockRejectedValue(new Error('core offline'));
227+
228+
render(
229+
<CoreStateProvider>
230+
<Consumer />
231+
</CoreStateProvider>
232+
);
233+
234+
await waitFor(() =>
235+
expect(warnSpy).toHaveBeenCalledWith('[core-state] poll failed (attempt 1/5):', {
236+
message: 'core offline',
237+
})
238+
);
239+
} finally {
240+
warnSpy.mockRestore();
241+
}
242+
});
243+
219244
it('backfills snapshot.currentUser from auth.user when currentUser is missing', async () => {
220245
fetchSnapshot.mockResolvedValue(
221246
makeSnapshot({
@@ -350,3 +375,15 @@ describe('CoreStateProvider — identity-change cache clearing', () => {
350375
});
351376
});
352377
});
378+
379+
describe('coreStatePollFailureWarningMessage', () => {
380+
it('logs bounded bootstrap failures and one suppression notice', () => {
381+
expect(coreStatePollFailureWarningMessage(0)).toBeNull();
382+
expect(coreStatePollFailureWarningMessage(1)).toBe('[core-state] poll failed (attempt 1/5):');
383+
expect(coreStatePollFailureWarningMessage(5)).toBe('[core-state] poll failed (attempt 5/5):');
384+
expect(coreStatePollFailureWarningMessage(6)).toBe(
385+
'[core-state] poll failed repeatedly; suppressing further warnings until core state recovers:'
386+
);
387+
expect(coreStatePollFailureWarningMessage(7)).toBeNull();
388+
});
389+
});

app/src/services/api/__tests__/rewardsApi.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('rewardsApi', () => {
101101

102102
const snapshot = await rewardsApi.getMyRewards();
103103

104-
expect(apiClient.get).toHaveBeenCalledWith('/rewards/me');
104+
expect(apiClient.get).toHaveBeenCalledWith('/rewards/me', { timeout: 15000 });
105105
expect(snapshot.discord.membershipStatus).toBe('not_linked');
106106
expect(snapshot.summary.totalCount).toBe(8);
107107
});

app/src/services/api/rewardsApi.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { ApiResponse } from '../../types/api';
22
import type { RewardsAchievement, RewardsSnapshot } from '../../types/rewards';
33
import { apiClient } from '../apiClient';
44

5+
const REWARDS_SNAPSHOT_TIMEOUT_MS = 15_000;
6+
57
function asRecord(value: unknown): Record<string, unknown> | null {
68
return value && typeof value === 'object' && !Array.isArray(value)
79
? (value as Record<string, unknown>)
@@ -106,7 +108,9 @@ export function normalizeRewardsSnapshot(payload: unknown): RewardsSnapshot {
106108

107109
export const rewardsApi = {
108110
async getMyRewards(): Promise<RewardsSnapshot> {
109-
const response = await apiClient.get<ApiResponse<unknown>>('/rewards/me');
111+
const response = await apiClient.get<ApiResponse<unknown>>('/rewards/me', {
112+
timeout: REWARDS_SNAPSHOT_TIMEOUT_MS,
113+
});
110114
if (!response.success) {
111115
throw {
112116
success: false,

0 commit comments

Comments
 (0)