import { useEffect, useState } from 'react' import { useBillingStore } from '@/store/billingStore' import { usageApi } from '@/api/usage' const CACHE_TTL_MS = 60 * 1000 interface CacheEntry { used: number timestamp: number } const cache = new Map() /** Clear the usage cache (call on logout to prevent stale data across users). */ export function clearUsageCache() { cache.clear() } export interface FeatureLimitResult { used: number limit: number | null /** null when limit is null (unlimited) or unknown */ percentage: number | null isAtLimit: boolean isLoading: boolean } function coerceLimit(raw: unknown): number | null { if (typeof raw === 'number' && Number.isFinite(raw)) return raw if (raw === null || raw === undefined) return null // The store types planLimits as Record; the backend // currently returns numbers, but defensively handle string ints too. if (typeof raw === 'string') { const n = Number(raw) return Number.isFinite(n) ? n : null } return null } /** * Returns progress against a quantitative plan limit. * * `limit` comes from `useBillingStore.planLimits[field]`, which is read * synchronously from the store. `used` is fetched lazily from * `GET /api/v1/usage/{field}` on mount and cached for 60s in a module-level * map keyed by field. * * Render is non-blocking: the hook returns `isLoading=true` (with `used=0`) * until the usage fetch resolves. On 404 or any error the hook degrades to * `used=0` with `isLoading=false` rather than surfacing the error — the * `/usage/{field}` endpoint is not yet implemented on the backend (planned). */ export function useFeatureLimit(field: string): FeatureLimitResult { const limit = useBillingStore((state) => coerceLimit(state.planLimits[field])) const [state, setState] = useState(() => { const existing = cache.get(field) const fresh = existing && Date.now() - existing.timestamp < CACHE_TTL_MS return { field, used: fresh ? existing.used : 0, isLoading: !fresh, } }) useEffect(() => { const existing = cache.get(field) if (existing && Date.now() - existing.timestamp < CACHE_TTL_MS) { // eslint-disable-next-line react-hooks/set-state-in-effect -- sync local hook state with fresh module cache on field change setState({ field, used: existing.used, isLoading: false }) return } let cancelled = false setState({ field, used: 0, isLoading: true }) usageApi .getCount(field) .then((result) => { if (cancelled) return cache.set(field, { used: result.used, timestamp: Date.now() }) setState({ field, used: result.used, isLoading: false }) }) .catch(() => { // TODO: backend /usage/{field} endpoint not yet implemented (planned). // 404s and other errors degrade to used=0 silently — no toast. if (cancelled) return setState({ field, used: 0, isLoading: false }) }) return () => { cancelled = true } }, [field]) const used = state.field === field ? state.used : 0 const isLoading = state.field === field ? state.isLoading : true const percentage = limit === null || limit <= 0 ? null : Math.min(100, Math.round((used / limit) * 100)) const isAtLimit = limit !== null && used >= limit return { used, limit, percentage, isAtLimit, isLoading } } export default useFeatureLimit