fix(tradein/v2): гонка при клике по строке «Предыдущие оценки» — отчёт не загружался #2428
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2428
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-cacheview-restore-race"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Живьём воспроизведено на проде (Playwright, авторизованная сессия): клик по строке в оверлее «Предыдущие оценки» (feature из #2424) обновлял URL на
?id=<uuid>, закрывал оверлей — но АДРЕС/ОТЧЁТ/ДАТА оставались «—», запроса на загрузку оценки не было вовсе (network tab пуст).Root cause:
router.replace(\?id=...`)— асинхронный client-side nav (RSC round-trip),window.location.searchНЕ обновляется синхронно.readUrlId()вpage.tsxчитаетwindow.location.searchнапрямую (useSearchParamsнамеренно не используется — ломаетnext buildчерез Suspense, см. комментарий в коде). Клик-хендлер тут же зовётsetNav(0), что форсирует синхронный ре-рендер в том же тике — раньше, чем завершится асинхронныйrouter.replace. На этом ре-рендереreadUrlId()читает ещё старый URL →urlId = null→restoreActive = false` → запрос на оценку не срабатывает. Компонент не подписан на изменения URL реактивно, поэтому это состояние замирает навсегда.Fix (после 3 раундов adversarial review):
pendingRestoreId— синхронный override, выставляемый в том же обработчике клика, что иrouter.replace, так что рендер сразу получает верный id, не дожидаясьreadUrlId().handleNew()(«НОВАЯ ОЦЕНКА» после 404) не сбрасывалpendingRestoreId→ permanent lock после клика по битой ссылке.nullвhandleNew()конфликтовал сpendingRestoreId ?? readUrlId()—nullтам означал одновременно «нет override» и «принудительно пусто», так что послеhandleNew()следующий рендер тут же проваливался обратно вreadUrlId(), который на тот момент всё ещё возвращал старый плохой id.undefined(нет override) /null(принудительно пусто) /string(override на id). Устраняет двусмысленность полностью — проверено вручную пошагово для обоих путей (клик по валидной строке,handleNew()после 404).Test plan
tsc --noEmitчистыйRefs #2424
Живая проверка в браузере (клик по строке "Предыдущие оценки" на проде) подтвердила: router.replace() — асинхронный client-nav (RSC round-trip), window.location.search не обновляется синхронно, а readUrlId() читает window.location.search напрямую (useSearchParams намеренно не используется — ломает next build через Suspense). Единственный надёжный источник urlId в синхронном ре-рендере — override-state, выставленный в том же тике, что и клик. Предыдущая версия (`pendingRestoreId ?? readUrlId()`) использовала null для двух разных смыслов сразу: "нет override" и "принудительно пусто". handleNew() выставлял null именно во втором смысле (сброс после 404), но ?? тут же трактовал null как "нет override" и падал обратно на readUrlId() — который на тот момент ещё возвращал старый плохой id (router.replace("/v2") ещё не успел домотать). Итог: "НОВАЯ ОЦЕНКА" после битой ссылки могла не сбрасывать ошибку. Три состояния убирают двусмысленность: undefined = нет override (readUrlId рулит), null = принудительно пусто, string = override на конкретный id.