Типизация и детерминированное разрешение конфликтов при кэшировании React Server Action...
Типизация и детерминированное разрешение конфликтов при кэшировании React Server Actions: staleTimes, revalidate и кастомные GC-стратегии Когда видишь staleTimes и revalidate в React Server Actions, первое желание — поставить числа побольше и забыть. Но на production это заканчивается сессионными багами, которые не воспроизводятся локально. Часто разработчики воспринимают эти настройки как магию, а потом ловят stale data, которую не могут объяснить. Типизация как контракт кэша Делай дискриминационное объединение для каждого кеш-слота: type CacheEntry<T> = | { status: 'fresh'; data: T; timestamp: number } | { status: 'stale'; data: T; lastRevalidate: number } | { status: 'invalid' }; Это заставит компилятор проверить статус перед использованием данных. Никаких runtime-сюрпризов с невалидными записями. Типизируй staleTimes как контракт, а не таймер: type StaleConfig = { maxAge: number; // время свежести данных staleWhileRevalidate: number; // окно показа старых с перезапросом gcAfter?: number; // принудительная чистка }; Кастомная GC-стратегия для долгоживущих данных Для профиля пользователя: maxAge — 10 минут, staleWhileRevalidate — 1 час, gcAfter — 2 часа. Полный sweep убивает производительность. Используй вероятностное удаление: function gcSweep(cache: Map<string, CacheEntry<unknown>>, config: StaleConfig) { const now = Date.now(); for (const [key, entry] of cache) { if (entry.status === 'invalid' || (entry.timestamp + config.gcAfter! < now && Math.random() < 0.1)) { cache.delete(key); } } } Шанс 10% на удаление просроченной записи не даёт GC-взрыва и держит кеш в чистоте без блокировок. Детерминированное разрешение конфликтов Два способа. Первый — deduped fetch с типизированным локом: const lock = new Map<string, Promise<unknown>>(); async function dedupedFetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> { if (!lock.has(key)) { lock.set(key, fetcher().finally(() => lock.delete(key))); } return lock.get(key)!; } Второй — версионность: храни счётчик версий в кеш-слоте, при записи проверяй, что версия не устарела. Это решает гонки при параллельных revalidate. Типичная ошибка: не типизировать состояние кеша и полагаться на runtime-проверки. В production это ведёт к race conditions, которые не видны в dev-режиме с одним пользователем. Строй явный жизненный цикл: через revalidate с проверкой типов решай актуальность данных, а GC делай фоновым и вероятностным. Вывод: Явная типизация состояний кэша, декларативный staleTimes-контракт и вероятностный GC — это не про микрооптимизацию, а про детерминированное управление потоком данных в production-grade приложениях.