refactor(ts): close the 4 client any's + document a best-effort catch (Phase 1b)
parseJsonSafe/upload-body → a structured ResponseBody type (typed error envelope + index sig) instead of any, no cast churn; declineRecommendation → Recommendation; snowballPlans endpoint typed so the hook drops its (d: any). Annotated the Spending settings-load catch as best-effort (Finding #8) — cosmetic options fall back to defaults, no mount-time nag. Client 0 any's. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
398caec29d
commit
57e7ede670
|
|
@ -54,11 +54,16 @@ const MUTATING_METHODS = ['POST', 'PUT', 'DELETE', 'PATCH'];
|
|||
// still bounding a truly dead request so it surfaces as an error, not a freeze.
|
||||
const REQUEST_TIMEOUT_MS = 120_000;
|
||||
|
||||
async function parseJsonSafe(res: Response): Promise<any> {
|
||||
// A parsed JSON response body. The error-envelope fields the client reads are
|
||||
// typed; any other (success) field passes through the index signature. Avoids an
|
||||
// `any` at the parse boundary without forcing casts at every read site.
|
||||
type ResponseBody = { message?: string; error?: string; code?: string; details?: unknown[]; token?: string } & Record<string, unknown>;
|
||||
|
||||
async function parseJsonSafe(res: Response): Promise<ResponseBody | null> {
|
||||
if (res.status === 204) return null;
|
||||
const text = await res.text();
|
||||
if (!text) return null;
|
||||
try { return JSON.parse(text); } catch { return null; }
|
||||
try { return JSON.parse(text) as ResponseBody; } catch { return null; }
|
||||
}
|
||||
|
||||
async function _fetch<T = unknown>(method: string, path: string, body?: unknown, _retried = false): Promise<T> {
|
||||
|
|
@ -192,8 +197,8 @@ export const api = {
|
|||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) {
|
||||
let data: any = {};
|
||||
try { data = await res.json(); } catch {}
|
||||
let data: ResponseBody = {};
|
||||
try { data = await res.json(); } catch { /* non-JSON error body */ }
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return {
|
||||
|
|
@ -319,7 +324,7 @@ export const api = {
|
|||
subscriptionTransactionMatches: (params: QueryParams = {}) => get<SubscriptionTx[] | { transactions?: SubscriptionTx[] }>(`/subscriptions/transaction-matches${queryString(params)}`),
|
||||
updateSubscription: (id: Id, data: Body) => _fetch('PATCH', `/subscriptions/${id}`, data),
|
||||
createSubscriptionFromRecommendation: (data: Body) => post<{ id?: number; name?: string }>('/subscriptions/recommendations/create', data),
|
||||
declineRecommendation: (recommendation: any) => post('/subscriptions/recommendations/decline', {
|
||||
declineRecommendation: (recommendation: Recommendation) => post('/subscriptions/recommendations/decline', {
|
||||
decline_key: recommendation?.decline_key || recommendation,
|
||||
catalog_id: recommendation?.catalog_match?.id,
|
||||
merchant: recommendation?.merchant,
|
||||
|
|
@ -348,7 +353,7 @@ export const api = {
|
|||
saveSnowballSettings: (data: Body) => _fetch<SnowballSettings>('PATCH', '/snowball/settings', data),
|
||||
saveSnowballOrder: (items: Body) => _fetch('PATCH', '/snowball/order', items),
|
||||
snowballProjection: (params: QueryParams = {}) => get<SnowballProjection>(`/snowball/projection${queryString(params)}`),
|
||||
snowballPlans: () => get('/snowball/plans'),
|
||||
snowballPlans: () => get<{ plans?: unknown[] }>('/snowball/plans'),
|
||||
snowballActivePlan: () => get('/snowball/plans/active'),
|
||||
startSnowballPlan: (data: Body) => post('/snowball/plans', data),
|
||||
updateSnowballPlan: (id: Id, d: Body) => _fetch('PATCH', `/snowball/plans/${id}`, d),
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export function useSnowballActivePlan() {
|
|||
export function useSnowballPlans() {
|
||||
return useQuery({
|
||||
queryKey: ['snowball-plans'],
|
||||
queryFn: () => api.snowballPlans().then((d: any) => d?.plans ?? []).catch(() => []),
|
||||
queryFn: () => api.snowballPlans().then(d => d?.plans ?? []).catch(() => []),
|
||||
staleTime: 1000 * 60 * 2,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -710,6 +710,8 @@ export default function SpendingPage() {
|
|||
}, [txPage, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
// best-effort: the page-options are cosmetic; on a load failure we fall back
|
||||
// to defaults rather than block the page or nag with a mount-time toast.
|
||||
api.settings().then(s => setSpendingSettings((s as Record<string, unknown>) || {})).catch(() => {});
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue