Skip to content

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

Description

@warbr1ng3r

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

goNeg = 34, goPos = 35, POPSTATE = 69, push = 0, replace = 0   // anyPushOrReplace = false; растёт ~47 popstate/сек

Reproduction

Один self-contained файл (зависимости с CDN), запуск в реальном браузере:

<!doctype html><html><head><meta charset=utf-8><title>@effector/router 1.2.0 back() re-entrancy</title></head><body>
<pre id=out>running…</pre>
<script type=module>
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);
})();
</script></body></html>

Запуск: 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

Лог одного цикла:

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 ...   // бесконечно

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions