[Bug]: Infinite back() loop — history.block re-intercepts its own retryNativeFx go() (single router, no app navigation)
- Dominant language
- TypeScript
- Stars
- 8
- Forks
- 2
- Avg merge
- 13h 44m
- Merged PRs (30d)
- 1
Description
### Package
@effector/router (core)
### What happened?
Один нативный `back()` при активном `history.block` (дефолтный `historyAdapter`) уходит в **бесконечный цикл** и вешает приложение. Никакой навигации со стороны приложения при этом не происходит.
Механизм: `retryNativeFx` (`transition.retry()`, dist `~index.js:724`) переприменяет удержанный POP **сырым `history.go`**, не обёрнутым в `runWithoutBlocking`. Активный blocker (`subscribeHistoryFx`, `~index.js:969`) перехватывает **этот же** retry-`go` как новую нативную навигацию → откатывает его → снова `retry` → бесконечно.
**Ожидается:** `retry()` должен обходить blocker (как `runWithoutBlocking` для `push`/`replace`), либо blocker должен приостанавливаться на время retry. Один `back()` должен дать один `POP`.
**Фактически:** бесконечный `go(-1)`/`go(1)` пинг-понг между двумя локациями; `history.length` стабилен; `push/replace = 0` (навигация приложения не участвует).
Связано с #109 (там потеря POP при конкурирующем `replace`) — вероятно, это более фундаментальная причина: конкурирующая навигация не нужна.
### Logs / screenshots
```text
goNeg = 34, goPos = 35, POPSTATE = 69, push = 0, replace = 0 // anyPushOrReplace = false; растёт ~47 popstate/сек
```
### Reproduction
Один self-contained файл (зависимости с CDN), запуск в реальном браузере:
```html
@effector/router 1.2.0 back() re-entrancy
running…
import { createRouter, createRoute, historyAdapter } from 'https://esm.sh/@effector/router@1.2.0';
import { createBrowserHistory } from 'https://esm.sh/history@5.3.0';
const out=document.getElementById('out');
const c={push:0,replace:0,goNeg:0,goPos:0,pop:0}; const log=[];
const H=window.history, _p=H.pushState,_r=H.replaceState,_g=H.go,_b=H.back;
H.pushState=function(...a){c.push++;if(log.length<400)log.push('PUSH '+a[2]);return _p.apply(this,a);};
H.replaceState=function(...a){c.replace++;if(log.length<400)log.push('REPLACE '+a[2]);return _r.apply(this,a);};
H.go=function(d){d<0?c.goNeg++:d>0&&c.goPos++;if(log.length<400)log.push('GO('+d+') @'+location.pathname);return _g.call(this,d);};
H.back=function(){if(log.length<400)log.push('history.back() @'+location.pathname);return _b.call(this);};
window.addEventListener('popstate',()=>{c.pop++;if(log.length<400)log.push('POPSTATE @'+location.pathname);});
const history=createBrowserHistory();
const routeA=createRoute({path:'/a'}), routeB=createRoute({path:'/b'});
const router=createRouter({base:'',routes:[routeA,routeB]});
router.setHistory(historyAdapter(history));
const s=ms=>new Promise(r=>setTimeout(r,ms));
(async()=>{ await s(150); routeA.open(); await s(150); routeB.open(); await s(250);
c.push=c.replace=c.goNeg=c.goPos=c.pop=0; log.length=0;
window.history.back(); // единственный триггер, дальше никакой навигации
await s(1500);
out.textContent=JSON.stringify({afterPath:location.pathname,counts:{...c},anyPushOrReplace:c.push>0||c.replace>0,logSample:log.slice(0,26)},null,2);
})();
```
Запуск: `python3 -m http.server 8099 --bind 127.0.0.1` → открыть `http://127.0.0.1:8099/`.
Шаги: один `createRouter` + `createBrowserHistory` + дефолтный `historyAdapter`, роуты `/a`,`/b`; собрать `/a → /b` через `route.open`; затем один `window.history.back()` — больше ничего.
### Environment
- `@effector/router`: `1.2.0`
- `history`: `5.3.0`
- Браузер: Chromium (реальный)
- `createMemoryHistory` этот дефект НЕ ловит: там `back` синхронный, нет popstate-revert.
### Logs / screenshots
Лог одного цикла:
```text
history.back() @/b
POPSTATE @/a // back закоммитился в /a
GO(1) @/a // blocker: revert -> /b
POPSTATE @/b
GO(-1) @/b // retryNativeFx: transition.retry() -> /a
POPSTATE @/a
GO(1) @/a ... GO(-1) @/b ... // бесконечно
```
Contributor guide
Research direction
Start with the core historyAdapter and trace retryNativeFx around transition.retry() in dist ~index.js:724, then inspect subscribeHistoryFx around ~index.js:969 and the existing runWithoutBlocking behavior. Reproduce the single-browser-back case, and consider the issue resolved when one back produces one POP without an ongoing go(-1)/go(1) loop or application push/replace navigation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100