fix(web): lift route repair out of TreePane into the shell #36
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/35"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Route repair was implemented as an effect inside
TreePane— a conditionally-mounted component — and navigated with a push. All three defects in #35 follow from that one placement, so they are fixed together by moving the rule to where it belongs.What changed
New
web/src/routerepair.ts. A purerepairRoute(route, facts)judges module, language and page against the module registry and the page tree, and returns the path the URL must be replaced with (or null when the route is sound). A shell-level effect applies it. It waits for the tree, because the site's language is the tree's to report — until it arrives there is no route to repair to.sessionStorage) or a future module declaringtree: falseno longer leaves a stale bookmark stranded on a permanent error pane. The module body is additionally held back until the route is worth rendering: a route awaiting repair can only fetch a 404 or a 400, so the doomed request is never issued and the error pane never flashes.selectPagegains an explicit history intent: a tree click pushes (a user navigation Back must undo), while a follow of a server mutation replaces, because the URL it would otherwise leave behind names a file the site no longer has./page/de/about.mdlands on/page/en/about.mdrather than dropping the page.The mutation-follow seam. Rename, move and bundle conversion change the tree and the route together and cannot do it atomically — the fresh tree lands while the URL still names the old path, and Solid flushes effects synchronously on that write, so the repair fired in between and pruned the editor to the empty state.
follow()suspends repair for the length of such a follow, stating the invariant honestly (the route and the tree are allowed to disagree while one is in flight) and re-judging the now-consistent pair on the way out, so a follow that left a bad route behind is still caught. This is what removes the spurious empty-state history entry every rename, move and conversion used to leave behind.Also fixed — the two defects recorded under "Not blocking" in the issue:
saveAndContinuewas re-entrant: a double-click on "Save & continue" posted the same page file twice, the exact read-modify-write race its sequential-save loop is documented to prevent. It now drops a re-entrant call (before taking the one-shotretrycontinuation, so the pending navigation survives) and the prompt shows its work and stops taking answers while saves are on the wire.One inherited asymmetry, called out deliberately
An unknown language keeps a page the tree has (
/page/de/about.md→/page/en/about.md), but an unknown module drops everything behind it (/bogus/en/about.md→/page/en). The latter is the behavior #33 shipped and its test pins it; changing it is not in scope for this issue, so it was left alone rather than quietly redefined. Worth a look if you disagree.Verification
Written test-first: the three repros from the issue were encoded as tests against the broken code and observed to fail for the right reasons (URL never repairs /
history.lengthgrows / lang left broken) before any fix was written.web/src/test/ShellRouting.test.tsx— 5 new tests: repair with the tree collapsed, repair costing no history entry, a rename leaving no empty-state entry behind, an unknown lang keeping its page, and an unknown lang plus a nonexistent page repaired in one step.web/src/test/routerepair.test.ts— 16 table-driven unit tests for the pure judgment, including the tree-not-loaded case that must repair nothing.Full gate green:
tsc --noEmitclean,npm run buildOK, 418 web tests pass (389 on base),go vetclean, all Go packages pass.Closes #35
Route repair lived in TreePane -- a conditionally-mounted component -- and navigated with a push. Three defects followed from that one placement. A URL naming a page the tree does not have was only repaired while the tree column happened to be mounted: collapse the tree (persisted across reloads) and a stale bookmark stranded the editor on a permanent error pane, as would any module declaring `tree: false`. The repair also pushed, so Back returned to the bad URL, which repaired and pushed again -- the editor could never navigate back past it -- and every rename, move and bundle conversion left a spurious empty-state entry in history on the way to the new path. An unknown language was never repaired at all: the page fetch 400s ("unknown lang") and the editor parked on a route no tree click could correct. Repair is a rule about the route, so it now lives at the route's level, in routerepair.ts: a pure `repairRoute` judging module, language and page against the registry and the tree, and a shell-level effect that REPLACES. It runs whether or not any tree is mounted, and the module body is held back until the route is worth rendering -- a route awaiting repair can only fetch a 404 or a 400, so the doomed request is never issued and the error pane never flashes. Rename, move and bundle conversion change the tree and the route together and cannot do it atomically: the fresh tree lands while the URL still names the old path. `follow()` suspends repair for the length of such a mutation-follow, stating the invariant honestly -- the two are allowed to disagree in that window -- and re-judges the now-consistent pair on the way out. selectPage gains an explicit history intent: tree clicks push, follows of a server mutation replace, because the URL they would leave behind names a file the site no longer has. Also fixes the two defects recorded alongside these: saveAndContinue was re-entrant (a double-click on "Save & continue" posted the same page file twice, the very race its sequential loop exists to prevent), and a pending preview-bridge selection was only ever cleared when its own page loaded, so navigating away first left it lying in wait to hijack that page's element selection on a later visit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Landing audit — PR #36
Verdict: PASS (one non-blocking note).
Verification signal relied on: Forgejo Actions CI —
ci / build-and-test (pull_request)→ success (6m01s, run 26). Not re-run locally; the PR is mergeable intomainwith no conflicts.Checks performed
fix(web): lift route repair out of TreePane into the shellis Conventional Commits. Body carries a workingCloses #35, so the merge will auto-close the issue and release theafk/35claim.createRouteRepair, so a collapsed tree column (or a futuretree: falsemodule) no longer strands a stale bookmark on a permanent error pane.replaces;selectPagegains an explicit push/replace intent so tree clicks still push and mutation-follows replace.repairRoutejudges language and page independently and keeps the survivors, so/page/de/about.mdlands on/page/en/about.mdrather than dropping the page.repairRoutehard-forces the tree's reported language. Verified this is correct for v1:internal/server/server.go:55declareslang string // configured language (v1: single). It will need revisiting if the site ever speaks more than one language, but it is not a bug today.saving()bail-out is correctly placed before the one-shotretrycontinuation is taken, so a dropped double-click does not strand the pending navigation.follow()uses a counter rather than a flag, so nested follows (a conversion inside a rename) cannot lift the suspension early.Non-blocking note — one overclaim in the PR body. The body states the doomed request "is never issued and the error pane never flashes." That holds for any repair after the tree has loaded, but not on a cold load:
repair.pending()is false whilelang === "", so on first paint of/page/en/ghost.mdthe module body renders andGET /api/pageis issued in parallel withGET /api/tree. If the 404 returns first,page-errorrenders until the tree lands and the repair replaces the route. This is transient and cosmetic, it is strictly better thanmain(where the same route is a permanent error page with the tree collapsed), and it matches the "flashes for one frame" aside the issue itself recorded — so it is not a blocker, just a claim worth not believing verbatim.Also verified in scope: the two "Not blocking" defects from the issue (re-entrant
saveAndContinue, the pending preview-bridge selection that could hijack a later visit) are both fixed and tested.