import { getRpcBaseUrl } from '@/services/rpc-client'; import type { GetGivingSummaryResponse as ProtoResponse } from '@/generated/client/worldmonitor/giving/v1/service_client'; import { createCircuitBreaker } from '@/utils'; import { getBootstrapHydrationState, getHydratedData } from '@/services/bootstrap'; import { GivingServiceClient } from '@/services/generated-rpc-clients'; import { EMPTY_PROTO_RESPONSE, GIVING_BREAKER_CACHE_KEY, GIVING_STALE_CEILING_MS, classifyGivingResponse, isCacheableGivingResponse, resolveGivingHydration, resolveGivingRefresh, } from './model'; import type { GivingFetchResult, GivingRefreshFailure, GivingSnapshot, } from './model'; export * from './model'; const client = new GivingServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args), }); const breaker = createCircuitBreaker({ name: 'Global Giving', cacheTtlMs: 30 * 60 * 1000, persistCache: true, persistentStaleCeilingMs: GIVING_STALE_CEILING_MS, }); let lastGoodSnapshot: GivingSnapshot | null = null; let lastRefreshFailure: GivingRefreshFailure | null = null; let fetchPromise: Promise | null = null; export async function fetchGivingSummary(): Promise { const now = Date.now(); const hydrated = getHydratedData('giving') as ProtoResponse | undefined; if (hydrated) { const hydration = resolveGivingHydration( hydrated, getBootstrapHydrationState().tiers.slow.source, now, ); if (hydration.lastGood) lastGoodSnapshot = hydration.lastGood; lastRefreshFailure = hydration.refreshFailure; if (hydration.immediateResult) return hydration.immediateResult; } if (fetchPromise) return fetchPromise; fetchPromise = (async (): Promise => { let nonV2Refresh: ProtoResponse | null = null; const response = await breaker.execute(async () => { let responseFailure: GivingRefreshFailure | null = null; try { const fresh = await client.getGivingSummary({ platformLimit: 0, categoryLimit: 0, }); const classified = classifyGivingResponse(fresh); if (classified.kind !== 'v2') { nonV2Refresh = fresh; responseFailure = classified.kind; throw new Error(`Giving refresh returned ${classified.kind} data`); } lastRefreshFailure = null; return fresh; } catch (error) { lastRefreshFailure = responseFailure ?? 'transport'; throw error; } }, EMPTY_PROTO_RESPONSE, { cacheKey: GIVING_BREAKER_CACHE_KEY, shouldCache: isCacheableGivingResponse, staleRefreshMode: 'await', forceRefresh: lastRefreshFailure !== null, }); const resolvedAt = Date.now(); const classified = classifyGivingResponse(response, resolvedAt); if (classified.kind === 'v2') { lastGoodSnapshot = classified.snapshot; if (breaker.getDataState().mode !== 'live' && lastRefreshFailure) { return resolveGivingRefresh(null, lastGoodSnapshot, resolvedAt, lastRefreshFailure); } return resolveGivingRefresh(response, null, resolvedAt, null); } if (nonV2Refresh) { return resolveGivingRefresh( nonV2Refresh, lastGoodSnapshot, resolvedAt, lastRefreshFailure, ); } const failure = lastRefreshFailure ?? classified.kind; return resolveGivingRefresh(null, lastGoodSnapshot, resolvedAt, failure); })().finally(() => { fetchPromise = null; }); return fetchPromise; }