Track detail: await history.go popstates before re-render after gap-fill revert/save

reopenDetail fired history.go(-count) then re-rendered synchronously, so the
async map/chart build raced the popstate unwind: the pending popstates
desynced the back stack and bumped the detail render token, and the render
bailed at the token check - map and charts came up blank until the next
event repainted them. Make reopenDetail async and await the popstate(s)
before showSessionDetail, the same resolve-on-popstate pattern as closeThen
(no setTimeout/setInterval).
This commit is contained in:
4aiman 2026-09-29 16:03:52 +03:00
commit fd178ad97d
2 changed files with 32 additions and 2 deletions

View file

@ -96,3 +96,20 @@ reopen -> убрал достройку -> зум/таскание; буфер
Проверка: воспроизвести тот же сценарий, снять logcat - ожидаем, что `tile memory limits
exceeded` резко упадёт/исчезнет, джанк уйдёт; POI на малом зуме - мало маркеров.
Результат (build 480): пользователь - "всё ок".
## Баг: после "убрать достройку" пусто (карта+графики) до следующего действия
Симптом: revert достройки -> графики и карта пропадают, из экрана трека не выкидывает,
появляются только после следующего действия ("Отмена"). Стек окон + гонка.
Корень: `reopenDetail` (main.js:1176) стрелял `history.go(-count)` (асинхронные popstate) и
СРАЗУ синхронно `showSessionDetail`. showSessionDetail пушит свою history-запись, потом
отложенные popstate от go(-count) раскручивают уже ЕЁ и десинхронят стек; при fallthrough в
renderScreen второй showSessionDetail бампает `_sessionDetailRender`, и первый async-рендер
карты/графиков бросается на проверке токена (5411) -> пусто до следующего события. Ровно тот
же класс, что чинил `closeThen()` (комментарий там про это).
Фикс: `reopenDetail` сделан async и ДОЖИДАЕТСЯ popstate от history.go перед showSessionDetail -
через Promise + слушатель popstate (как `closeThen`), без setTimeout/setInterval.
Проверка: revert на устройстве, по [SD]-логам один прогон showSessionDetail, карта+графики
сразу на месте.

View file

@ -1173,7 +1173,7 @@ function appAlert(message, okText = "OK") {
// balance the history stack, then reopen it fresh. Mirrors the recalc/refine pattern
// but pops down to the detail modal so it also works while the map is fullscreen —
// re-rendering in place instead of stacking a second overlay (which broke the map/charts).
function reopenDetail(sessionId) {
async function reopenDetail(sessionId) {
let count = 0;
while (winStack.length > 0) {
let top = winStack.pop();
@ -1181,7 +1181,20 @@ function reopenDetail(sessionId) {
try { top.close(); } catch {}
if (top.type === "modal") break;
}
if (count > 0) { _skipPops = count; history.go(-count); }
// AWAIT the history.go(-count) popstate(s) before re-rendering. Rendering synchronously
// after history.go races it: showSessionDetail pushes its own history entry, then the
// pending popstates unwind THAT entry and desync the stack, and the async map/chart build
// comes up blank until the next event repaints it (the "graphs+map gone until Cancel" bug).
// Same fix as closeThen() — resolve on the popstates, never a timeout.
if (count > 0) {
_skipPops = count;
await new Promise(res => {
let remaining = count;
let h = () => { if (--remaining <= 0) { window.removeEventListener("popstate", h); res(); } };
window.addEventListener("popstate", h);
history.go(-count);
});
}
showSessionDetail(sessionId);
}