Skip to content

fix(routing): three ways URL state stopped reaching the URL - #185

Merged
adamjohnwright merged 2 commits into
mainfrom
fix/settle-without-a-latch
Sep 9, 2026
Merged

fix(routing): three ways URL state stopped reaching the URL#185
adamjohnwright merged 2 commits into
mainfrom
fix/settle-without-a-latch

Conversation

@adamjohnwright

@adamjohnwright adamjohnwright commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

An adversarial review of #184 and #181. Three findings; the third is the serious one and came from neither issue.

1. settling was a latch, and latches leak

#184 put a boolean on UrlStateService: settle() raised it, the writer effect read it and lowered it. That only works if the writer always runs, exactly once, right after. It does not:

  • the writer returns early on the content routes, before it lowers the flag;
  • a default that already matches the current value changes no signal, so the writer never runs at all.

Either way the flag outlives its turn, and the reader's next write — a real choice — silently replaces their history instead of adding to it.

The writer now decides from the values: settle() records what the URL should say once its change has been applied, and the writer replaces only if it is about to write exactly that. Both sides go through one currentQueryParams(), so they agree by construction rather than by a comment.

The record is taken untracked. settle() is called from inside the details defaulting effect, so reading every param made that effect depend on tab: choosing Molecule re-ran the default, which set the tab straight back to details, and the click did nothing at all. Caught by e2e/back-button.spec.ts.

2. #181's scroll restore depends on RevealDirective's frame count

The hierarchy restores the reader's scroll on frame one; RevealDirective brings a selected row into view on frame two. That order is what makes block: 'nearest' compare against where the reader actually was, so a row already on screen is left alone. Reverse it and clicking a visible row scrolls twice again — the bug #181 fixed. Neither file said so, and both comments read as if their frame count were a local choice. Now noted in both.

3. Both route guards tested the wrong string

if (this.router.url.includes('content') || this.router.url.includes('query')) return;

The query string answers that as readily as the path does. sample holds a column name taken verbatim from the reader's own expression file, and analysis.service.ts:520 sets it automatically to the first column — so an expression file whose first column is called "GC content" puts ?sample=GC__content in the URL, and from that moment nothing the reader did was written to the URL again. Not selecting a node, not flagging, not changing tab. None of it survived a reload, and none of it was in any link they shared.

Now one exported predicate, testing whole segments of the path only. "query" is gone rather than fixed: the search page is content/query, already covered by the content test, and of the two words it is the likelier to turn up in somebody's data.

Verified

  • e2e/back-button.spec.ts — both directions: one Back leaves the pathway browser, and a tab the reader chose is still undone by Back. The second caught finding 1's untracked bug.
  • e2e/content-page-urls.spec.ts (new) — the three content pages keep their own addresses, which is what the guard is actually for and had no coverage at all; and the pathway browser still writes its URL under ?sample=GC__content. Confirmed to be a regression test rather than a description by putting the substring guard back: that case fails on it, passes on this.
  • 6 unit cases on isContentRoute, including the four URLs the old guard got wrong.
  • e2e/legacy-links.spec.ts, e2e/hierarchy-scroll.spec.ts, e2e/download-feedback.spec.ts — 11 passed.
  • 198 unit tests; check:types, check:dead, format:check clean; lint 654 → 653.

🤖 Generated with Claude Code

#184 put a `settling` boolean on the service: settle() raised it, the
writer effect read it and lowered it. That only works if the writer
always runs, once, right after. It does not:

  - the writer returns early on the content and search routes, before it
    ever lowers the flag;
  - a default that already matches the current value changes no signal,
    so the writer never runs at all.

Either way the flag outlives its turn, and the reader's next write -- a
real choice -- silently replaces their history instead of adding to it.

So the writer now decides from the values. settle() records the params
the URL should say once its change has been applied; the writer replaces
only if it is about to write exactly those, and clears the record either
way. Both sides go through one currentQueryParams(), so they agree by
construction rather than by a comment.

The record is taken untracked. settle() is called from inside the
details defaulting effect, so reading every param made that effect
depend on every param: choosing the Molecule tab re-ran the default,
which set the tab straight back to details. The URL never changed and
the click did nothing -- caught by e2e/back-button.spec.ts, which now
passes both ways again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright enabled auto-merge (squash) September 9, 2026 19:20
#181 restores the reader's scroll position on frame one; RevealDirective
brings a selected row into view on frame two. That order is what makes
`block: 'nearest'` compare against where the reader actually was, so a
row already on screen is left alone. Reverse it and clicking a visible
row scrolls twice again -- the bug #181 fixed.

Neither side said so, and both comments read as if their frame count
were a local choice. Noted in both, because either one being
"simplified" breaks the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit ce35363 into main Sep 9, 2026
6 checks passed
@adamjohnwright
adamjohnwright deleted the fix/settle-without-a-latch branch September 9, 2026 19:31
@adamjohnwright adamjohnwright changed the title fix(routing): decide "replace or add" from the URL, not from a flag fix(routing): three ways URL state stopped reaching the URL Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant