Status: done Completed: 2026-02-18 Priority: P1 Created: 2026-02-18 Depends-on: url-history-protocol, url-parsing-cross-platform, url-sync-runtime-state Owner: conductor
install-url-sync! in ui_routing2.cljc is wrapped in #?(:cljs ...) and directly calls js/window, js/setTimeout, and route-url/current-url. After the three prerequisite specs are done (protocol, CLJ parsing, runtime state), this function can be refactored to accept a URLHistoryProvider and work cross-platform.
The critical correctness issue this spec addresses: history stack corruption during back/forward navigation. The current on-save handler fires on every statechart save and may push URLs even when the state change was triggered by browser back/forward. This corrupts the stack so that back-2x/forward-2x doesn't return to the starting point.
#?(:cljs ...) wrapper from install-url-sync!:provider option: defaults to (browser-url-history) on CLJS, required on CLJ (throws clear error if missing)(some-> js/window .-location .-pathname) → (current-href provider)route-url/current-url → (current-href provider)route-url/push-url! → (-push-url! provider ...)route-url/replace-url! → (-replace-url! provider ...)(route-url/url->route-target) 0-arity → (route-url/url->route-target (current-href provider))js/setTimeout/js/clearTimeout with platform-appropriate debounce:
.addEventListener/.removeEventListener with (set-popstate-listener! provider ...)(set-popstate-listener! provider nil)@prev-url (the tracked URL BEFORE the browser moved) and the popped index, then sets nav-state:
{:browser-initiated? true
:pre-nav-index (current-index provider) ;; where we were
:popped-index popped-index ;; where browser moved to
:pre-nav-url @prev-url} ;; URL before browser moved
Note: current-href at popstate time returns the NEW url (browser already moved). @prev-url is the correct pre-nav reference.
nav-state):
browser-initiated? true AND new-url matches expected destination → skip push/replace, update prev-urlbrowser-initiated? true AND new-url does NOT match where browser went (busy guard rejected) → call go-forward! or go-back! on provider to undo the browser's navigation (compare popped-index vs pre-nav-index to determine direction)old-url is nil) → (-replace-url! provider new-url)(-push-url! provider new-url)nav-state after reading it:provider option get BrowserURLHistory automaticallyurl-sync-on-save 2-arity is removed (breaking change). All callers must use the 3-arity form that receives app. The install-url-sync-headless and url-sync-runtime-state specs together handle this.integration/fulcro/ui_routing2.cljc — install-url-sync! (major refactor), requires for route-url protocolpopstate fires with popped-index
→ save pre-nav-url from @prev-url (NOT current-href, which is already the new URL)
→ save pre-nav-index from (current-index provider)
→ set nav-state {:browser-initiated? true
:pre-nav-index pre-nav-index
:popped-index popped-index
:pre-nav-url pre-nav-url}
→ parse URL from (current-href provider) ;; this IS the destination URL now
→ find matching route target
→ send route-to! event to statechart
→ statechart transitions → on-save fires
→ on-save handles it (see below)
on-save fires
→ read nav-state, then clear it
→ compute new-url from deep-configuration->url
→ compute browser-url from (current-href provider)
BRANCH 1: browser-initiated + route ACCEPTED
(browser-initiated? AND new-url == browser-url)
→ just update prev-url to new-url
→ do NOT touch history (browser already moved)
BRANCH 2: browser-initiated + route DENIED
(browser-initiated? AND new-url != browser-url)
→ route was rejected by busy guard; browser moved but chart didn't
→ undo the browser navigation:
if popped-index < pre-nav-index → (go-forward! provider) ;; user went back, undo by going forward
if popped-index > pre-nav-index → (go-back! provider) ;; user went forward, undo by going back
→ restore prev-url to pre-nav-url
→ fire on-route-denied callback if provided
BRANCH 3: initial load (old-url is nil)
→ (-replace-url! provider new-url)
→ set prev-url to new-url
BRANCH 4: programmatic navigation (no nav-state, URL changed)
→ (-push-url! provider new-url)
→ set prev-url to new-url
Critical note on Branch 2: When go-forward!/go-back! is called to undo, the provider fires the popstate listener again. This must NOT cause a re-entrant loop. Solution: set a restoring? flag before calling go-forward!/go-back!, and have the popstate listener skip when restoring? is true. Clear after the undo completes.
(let [debounce-ms #?(:cljs 50 :clj 0)]
;; CLJS: js/setTimeout wrapping as before
;; CLJ: direct invocation (no timer needed for synchronous headless)
...)
install-url-sync! with SimulatedURLHistory provider works on CLJgo-back!/go-forward!url->route-target parses simulated URLs correctly on CLJinstall-url-sync! with no :provider auto-creates BrowserURLHistory on CLJSCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |