feat(flowchat): follow again when output catches up with a reader in the blank - #2263
Closed
wsp1911 wants to merge 1 commit into
Closed
feat(flowchat): follow again when output catches up with a reader in the blank#2263wsp1911 wants to merge 1 commit into
wsp1911 wants to merge 1 commit into
Conversation
…the blank Scrolling up by a pixel gave the follow away for good, and that is right only for a reader who left the live region. The reserved blank is up to `tailHoldMaxGapPx` under `hold-tail` and the whole gap under a pinned Turn, so a small scroll up can leave the reader a few hundred pixels off the tail with nothing hidden from them at all — until output grows past the bottom edge and they silently stop seeing it, with no affordance saying so. The snap back does not cover this. Its predicate is relative to the follow target, and under a pin the target sits inside the blank, so a reader above it is reported as having nothing to snap back from. `scrollTop > contentEnd` is the other edge of the same region, and between them they now cover it. So a watch runs for as long as the reader holds the viewport, keeping one bit between samples: whether the blank was on screen at the previous one. A crossing is that bit going from set to clear, and `resolveTailDepartureCrossing` decides who keeps the viewport by which side moved further — the content end rising to meet a stationary reader hands it back, a reader out-moving the content end keeps it. Streaming does not stop because the reader scrolled, so both sides usually move and the tie-break is doing real work. Not scoped to one crossing. That was written first and is wrong: a reader who climbs out of the blank, reads for a while and scrolls back down to sit in it again is in exactly the position the rule exists for, and had already spent the single crossing they were given. The watch also opens on any exit that finds none open rather than only the one that took the follow — every wheel notch exits, and only the first of them finds anything to give up. Three guards, each measured: - **A live gesture defers the crossing.** Over one session's twenty departures, nine left blank on screen and two of those ended `content-caught-up`: one a reader still for 1.6s and overtaken, one 320ms into a live gesture with the reader climbing 200px while content grew 237. The tie-break called the second for the content, correctly, and acting on it would have taken the viewport back mid-scroll. The latch is kept rather than spent, so the next sample judges the same transition again — a reader who carries on climbing is let go by a verdict that never needed the veto, one who has stopped is followed once the claim lapses. - **A travelling snap back is not the reader.** It crosses the same line from the wrong side, downwards through the blank, so samples taken while one is in flight update the offsets and take no verdict. - **Geometry read from two moments is not geometry.** `scrollTop` is clamped to `scrollHeight - clientHeight`, so no settled viewport is more than the spacer past the content end (`isTailBlankMeasurable`). A history prepend shifts the viewport by the height it inserted before that height is in the scroll range: measured, 14252px of shift against a content end still reading 989, a blank of 6239px in a transcript reserving a few hundred. Read at face value that is a reader deep in the blank, and the next sample would have pulled them out of the history they had just asked for. Resuming does not scroll. The blank closing is the two offsets meeting, so what remains is one sample of growth and the follow loop's ease covers it; a one-shot scroll would be a snap the reader can see for a correction they cannot. It also retires the pin, because the pin's reservation is the blank the reader just scrolled out of and restoring it would pull them back down to the offset they left. The unmount cleanup no longer depends on `closeTailWatch`. It is rebuilt whenever `getTailSpacerPx` changes identity, and a watch spans many renders by construction, so an effect tearing down with the callback closed every one of them a frame after it opened. `followOutput.tailWatch` and `tailWatchEnded` bracket the watch, the latter carrying `crossings` so one that resolved nothing can be told from one that resolved against the reader; `followOutput.tailCrossing` records each transition with its verdict, both raw deltas, and whether a gesture was live. Known gap added for the reader who scrolls out of the blank and stops at the content end — measured at four of nine watched departures, `readerMovedPx` tracking the blank height almost exactly — which neither this nor the snap back covers. Acting on it means reading intent from a resting position that has content in it, which the snap back's licence explicitly does not extend to.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scrolling up by a pixel gave the follow away for good, and that is right only
for a reader who left the live region. The reserved blank is up to
tailHoldMaxGapPxunderhold-tailand the whole gap under a pinned Turn, soa small scroll up can leave the reader a few hundred pixels off the tail with
nothing hidden from them at all — until output grows past the bottom edge and
they silently stop seeing it, with no affordance saying so.
The snap back does not cover this. Its predicate is relative to the follow
target, and under a pin the target sits inside the blank, so a reader above it
is reported as having nothing to snap back from.
scrollTop > contentEndisthe other edge of the same region, and between them they now cover it.
So a watch runs for as long as the reader holds the viewport, keeping one bit
between samples: whether the blank was on screen at the previous one. A
crossing is that bit going from set to clear, and
resolveTailDepartureCrossingdecides who keeps the viewport by which side moved further — the content end
rising to meet a stationary reader hands it back, a reader out-moving the
content end keeps it. Streaming does not stop because the reader scrolled, so
both sides usually move and the tie-break is doing real work.
Not scoped to one crossing. That was written first and is wrong: a reader who
climbs out of the blank, reads for a while and scrolls back down to sit in it
again is in exactly the position the rule exists for, and had already spent the
single crossing they were given. The watch also opens on any exit that finds
none open rather than only the one that took the follow — every wheel notch
exits, and only the first of them finds anything to give up.
Three guards, each measured:
nine left blank on screen and two of those ended
content-caught-up: one areader still for 1.6s and overtaken, one 320ms into a live gesture with the
reader climbing 200px while content grew 237. The tie-break called the second
for the content, correctly, and acting on it would have taken the viewport
back mid-scroll. The latch is kept rather than spent, so the next sample
judges the same transition again — a reader who carries on climbing is let go
by a verdict that never needed the veto, one who has stopped is followed once
the claim lapses.
the wrong side, downwards through the blank, so samples taken while one is in
flight update the offsets and take no verdict.
scrollTopis clamped toscrollHeight - clientHeight, so no settled viewport is more than the spacerpast the content end (
isTailBlankMeasurable). A history prepend shifts theviewport by the height it inserted before that height is in the scroll range:
measured, 14252px of shift against a content end still reading 989, a blank of
6239px in a transcript reserving a few hundred. Read at face value that is a
reader deep in the blank, and the next sample would have pulled them out of
the history they had just asked for.
Resuming does not scroll. The blank closing is the two offsets meeting, so what
remains is one sample of growth and the follow loop's ease covers it; a one-shot
scroll would be a snap the reader can see for a correction they cannot. It also
retires the pin, because the pin's reservation is the blank the reader just
scrolled out of and restoring it would pull them back down to the offset they
left.
The unmount cleanup no longer depends on
closeTailWatch. It is rebuiltwhenever
getTailSpacerPxchanges identity, and a watch spans many renders byconstruction, so an effect tearing down with the callback closed every one of
them a frame after it opened.
followOutput.tailWatchandtailWatchEndedbracket the watch, the lattercarrying
crossingsso one that resolved nothing can be told from one thatresolved against the reader;
followOutput.tailCrossingrecords each transitionwith its verdict, both raw deltas, and whether a gesture was live.
Known gap added for the reader who scrolls out of the blank and stops at the
content end — measured at four of nine watched departures,
readerMovedPxtracking the blank height almost exactly — which neither this nor the snap back
covers. Acting on it means reading intent from a resting position that has
content in it, which the snap back's licence explicitly does not extend to.