effector / effector/router

[Bug]: Infinite back() loop — history.block re-intercepts its own retryNativeFx go() (single router, no app navigation)

Open
#110 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.