Skip to content

fix: remove the 750ms LiveView latency simulator in dev - #3986

Merged
chasers merged 12 commits into
mainfrom
fix/stack-dev-latency-sim
Sep 17, 2026
Merged

chasers merged 12 commits into
mainfrom
fix/stack-dev-latency-sim

Conversation

@chasers

@chasers chasers commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Stack 9/9 — "Load more" pagination fixes.

  • Problem: dev holds back every LiveView message by 750 ms. The "Load more" spinner and every click feel slow.
  • Cause: the dev root layout calls liveSocket.enableLatencySim(750) on each page load.
  • Fix: remove that script. Same change as fix: remove the hardcoded 750ms LiveView latency simulator in dev #3901.
  • After merge: the value stays in sessionStorage. Run liveSocket.disableLatencySim() once in the browser console, or open a new tab.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL

@chasers
chasers added this pull request to stack #3983 September 14, 2026 19:29
@chasers
chasers force-pushed the fix/stack-dev-latency-sim branch from 73e2038 to 6bc6718 Compare September 17, 2026 19:30
@chasers
chasers force-pushed the fix/load-more-review branch 2 times, most recently from 3f42a01 to 670dd85 Compare September 17, 2026 19:50
@chasers
chasers force-pushed the fix/stack-dev-latency-sim branch from 6bc6718 to df58f52 Compare September 17, 2026 19:50
chasers and others added 12 commits September 17, 2026 14:06
The button drove its spinner from `phx-click-loading`, which LiveView applies
only while the event round trip is in flight. `load_events` hands the query to
`SearchQueryExecutor` and returns immediately, so the spinner stopped in
milliseconds while a BigQuery page query still took seconds.

`EventPagination` now carries a `loading_intent` and a fourth button state,
`:loading`. `load_events` marks the intent, and the page result and page error
handlers clear it. The button renders its spinner from that state. A hidden
button stays hidden, and `disabled={@State != :ready}` already covers
`:loading`, so a second click cannot queue behind the first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
A dropped page request was silent. The `with` fell through to
`{:noreply, socket}`, and the catch-all clause did the same. A request that the
guard or the cursor validation rejected looked like a page with no rows.

Both paths now log the intent, the cursor values and the guard state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
Every pagination test was Postgres-only, and `apply_timestamp_filter_rules/1`
and `cursor_condition/3` both take a different clause for BigQuery. A report
that the top button loads nothing came from a BigQuery source, and nothing
covered that path.

The new test stubs `bigquery_jobs_query`, returns a full page plus a sentinel
for the ranged query and a page of older rows for any query carrying
`TIMESTAMP_MICROS`, then clicks the top button and asserts both that the page
query ran and that the list grew by a page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQ8Y8zCvSKUFNPvkwH2WS4
A page request drops the query's own timestamp range so it can page past it,
and kept only a one-sided partition bound. The scan therefore ran to the end
of the table: on BigQuery that is every partition the source has ever had.

A page now moves by the width of the range in view, bounded below at 60
seconds. The query carries both a `TIMESTAMP_MICROS` bound and a
partition-date bound on each side, for `:timestamp`, `:pseudo` and Postgres
alike.

The LQL range grows by that same window on every click, rather than only when
the rows that came back happened to fall outside it, and an empty page shifts
the cursor by the window instead of clearing it. A quiet stretch no longer
stalls the button. `extend_timestamp_range/2` and `event_timestamp/2` are gone
with that change.

The buttons name the window they move by: "Load more (-10 minutes)" and
"Load more (+10 minutes)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQ8Y8zCvSKUFNPvkwH2WS4
Dialyzer fails the typing check on `event_timestamp_datetime/1`. The only
caller passes integer microseconds, so the `DateTime` and `NaiveDateTime`
clauses can never match. Convert the integer directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
A chart click already scrolled the page to the newest results, but running a
search did not, so submitting a query left the viewport wherever it was.

`start_search` now sets the same `scroll_to_bottom_on_result?` flag that
`datetime_update` uses, so the scroll happens once that search's first event
page lands.

The search button and the Enter key both route through one event:
`lql_editor_wrapper_hook.js` binds `monaco.KeyCode.Enter` and the submit
listener to the same `submitSearch/0`, which pushes `start_search`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQ8Y8zCvSKUFNPvkwH2WS4
A chart click scrolled the page to the newest results, but a search did not.
Scrolling was also split between the server and a `data-tailing` attribute
that the hook read on every update. A fresh load with a querystring never
scrolled. The follow-up diffs of a search (aggregates, pagination buttons)
each ran `restoreScrollAnchor/0` and undid the scroll.

- Every initial event page pushes `scroll-to-bottom`. The initial page is
  always the newest events in range.
- Tail appends push `scroll-to-bottom` too, so `scrollToLatest/0` and
  `data-tailing` come out.
- The hook holds a pending scroll until a frame runs it. It skips the anchor
  restore until then.

The server owns scroll intent. The hook owns viewport stability, which needs
element geometry that the server cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
A query with no `t:` filter now gets one on the first page request. The range
is the one the chart already draws. The aggregate query charts
`default_period_tick_count/1` periods back from now, so
`c:group_by(t::minute)` means the last 120 minutes.

The page window also uses that implied range, not a fixed 60 seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
Paging moves by a fixed window now, so a page that comes back short means the
window was quiet, not that the source has nothing more. The visibility rules
still came from the old sentinel model and read a short page as the end of the
line.

`page_cursors/3` takes the previous cursor from the rows it returned instead of
from the 101st sentinel row, so a quiet window no longer clears the cursor and
takes the top button away. `next_exhausted?` leaves `EventPagination` along
with `complete_initial/2`, `complete_page/3` and `complete_tail/2`, which
existed only to maintain it, and `next_page_available?/4` leaves the LiveView:
a range that ends in the future is fine and should not hide the button that
would move it further.

A button now hides in exactly two cases: the view is tailing, or it has no
cursor at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQ8Y8zCvSKUFNPvkwH2WS4
A short page now means the window was quiet, not that the source has no
older events. The e2e test still expected the button to disappear.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
- Store one page window per search in `EventPagination`. The label, the
  query, the cursor shift and the range growth all read it. The window no
  longer doubles on each click.
- Set the page window from a tail result too. `resume_tailing/1` resets the
  pagination, and the result that follows takes the tail path, so a soft
  pause, play and pause left the window nil and dropped the next page
  request. The window belongs to the search in view, not to the first page.
- Build the chart range in the search timezone. An implied range is no
  longer UTC written as local time.
- Keep a one-sided `t:` bound when a page request makes the range explicit.
- Cap the cursor of an empty "next" page at the request time.
- Clamp a "next" range extension at now. Each empty click used to push the
  range max another window into the future and re-run the aggregate.
- Drop a page result or page error that no request waits for.
- Clear the page spinner when a search fails to parse.
- Disable the other button while a page request runs.
- Remove the unused sentinel row: the extra fetched row, `fetch_limit/0`
  and the `has_more?` field that nothing reads.
- Remove dead clauses, a duplicate alias and inline comments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
The dev root layout called `liveSocket.enableLatencySim(750)` on every page
load. LiveView then held back every client message by 750ms, so every click
and form change in dev waited 750ms. The server was never slow.

This is the same change as #3901.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBPF1knvgfghTSdxuYryGL
@chasers
chasers removed this pull request from stack #3983 September 17, 2026 21:09
@chasers
chasers changed the base branch from fix/load-more-review to main September 17, 2026 21:09
@chasers
chasers force-pushed the fix/stack-dev-latency-sim branch from df58f52 to 10c699d Compare September 17, 2026 21:09
@chasers
chasers merged commit 9c9913b into main Sep 17, 2026
15 of 16 checks passed
@chasers
chasers deleted the fix/stack-dev-latency-sim branch September 17, 2026 21:12
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.

2 participants