Skip to content

refactor(viewer): extract scroll pending/pin machine into a pure reducer#41

Merged
denolfe merged 5 commits into
mainfrom
refactor/scroll-engine
Jul 25, 2026
Merged

refactor(viewer): extract scroll pending/pin machine into a pure reducer#41
denolfe merged 5 commits into
mainfrom
refactor/scroll-engine

Conversation

@denolfe

@denolfe denolfe commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Extracts the Viewer's scroll retry and pin orchestration into a pure reducer, giving the repo's highest-churn code a real unit-test seam. Behavior is preserved except one deliberate simplification (below).

The pending, pin, and frame-retry logic lived in a 160-line effect wired through four mutable refs, reachable only through slow integration tests. Six of the last eight feature PRs churned this surface, and the flicker and nav-correctness bugs both had to be chased without a unit seam.

Key Changes

  • Pure pendingReducer
    • New src/app/lib/pending-reducer.ts owns the machine state, { pending, isSwap }. It maps events (issueJump, pinJump, userScroll, frameTick) to effects (scrollBy, repositioned).
    • The reducer never reads live geometry. It is table-tested in isolation, no terminal harness required.
  • Viewer becomes a thin adapter
    • Viewer.tsx resolves the scrollbox geometry into a plain value and applies the reducer's effects. The four refs collapse to one state ref plus the unrelated notify refs.
  • Supersede simplification
    • A pending jump is cleared by a single userScroll path off the scroll watcher. A clamped no-op keypress (for example, down-arrow already at the bottom) no longer cancels a pin. This is the one intentional behavior change.
  • Docs
    • docs/ARCHITECTURE.md now describes the reducer seam instead of the old ref-based mechanism.

Design Decisions

  • Geometry stays out of the reducer. The adapter computes a Resolution (a plain { delta, reached } | null, the delta to scroll and whether the target is in range) each frame and feeds it in as a frameTick. This keeps the reducer pure and target-kind-agnostic, so the interface is the test surface. It mirrors the existing pure mapKey/dispatch split.
  • The settle condition is reached || fullyMounted, matching the original done || fullyMounted. A pin aimed past a now-shorter document still settles once mounting finishes, so the navigation cover (the opaque panel shown during a doc swap) still drops.
  • issueJump fires repositioned when it supersedes an in-flight swap pin. The original dropped the cover incidentally through the scroll watcher; the single-supersede design makes that explicit so the cover cannot stick.

Overall Flow

flowchart LR
  src[keypress / wheel / frame tick] --> adapter[Viewer adapter]
  adapter -- resolve geometry --> res[Resolution value]
  res --> reducer[pendingReducer]
  reducer -- effects --> adapter
  adapter -- scrollBy / repositioned --> box[scrollbox]
Loading

Previously the same logic ran inside one effect over four refs with no seam a test could reach. The reducer is now a pure function driven by the adapter, and every jump-into-unmounted-content path routes through it.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

Startup benchmark (--render test/exhaustive.md, linux-x64)

build mean ratio verdict
baseline (main@cfb3815) 784.2ms ± 33.6ms
PR 757.8ms ± 9.8ms 0.97× ✅ ok

Thresholds: warn ≥ 1.1×, fail ≥ 1.25×. Baseline built from main.

@denolfe
denolfe merged commit a2e8062 into main Jul 25, 2026
8 checks passed
@denolfe
denolfe deleted the refactor/scroll-engine branch July 25, 2026 02:09
denolfe added a commit that referenced this pull request Jul 26, 2026
- refactor: split breadcrumb and ancestor-stack vocabulary (#45) (4fc9a88)
- refactor(viewer): consolidate view state into a store hook (#44) (e39c971)
- refactor(viewer): extract nav-intent consumer into a pure dispatcher (#43) (8cbde71)
- refactor: consolidate heading/overlay geometry into a Fold module (#42) (bc77a33)
- refactor(viewer): extract scroll pending/pin machine into a pure reducer (#41) (a2e8062)
- feat(viewer): add a keyboard shortcut help drawer (#40) (cfb3815)
- feat(viewer): breadcrumb trail for relative-link navigation (#39) (65ffb0e)
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