Skip to content

feat(client): scheduler requests stats after first paint, with pending, not computed and error states (rows-first c4) - #1027

Closed
paddymul wants to merge 9 commits into
adr-002-rows-first-stats-deliveryfrom
feat/rowsfirst-c4-client-stats-scheduler
Closed

paddymul wants to merge 9 commits into
adr-002-rows-first-stats-deliveryfrom
feat/rowsfirst-c4-client-stats-scheduler

Conversation

@paddymul

@paddymul paddymul commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A client that advertises ?caps=stats_update (c2, #1025) to a deferred session gets a first initial_state with df_meta.stats.status == "pending" and merges the stats_update that answers a stats_request. Nothing sends that request, so the client sits on the stats-free frame with placeholder pinned rows and no host can select deferred delivery yet. The other statuses have no UI either: a session whose stats are not computed, or failed, shows the same empty pinned area as one that is loading, and nothing lets the user ask for stats that were skipped.

Phase and plan references

Plan 1 (plans/01-rows-first-stats-separate-plumbing.md) section 9 "Phase 4", with section 4.0 (client states), section 5 (Strategy C, the client scheduler) and section 2.6 (StateOrchestrator, #810). Plan 2 sections 3 and 4.1 for the request and reply shapes. Message names and shapes follow the server branch #1024.

Approach

Scheduler. StateOrchestrator.ts is rewritten around an IModel. It sends stats_request {stats_gen, scope: "raw"} through model.send and watches change:df_meta, change:df_data_dict, change:buckaroo_state and msg:custom.

  • It asks once the first infinite_resp has arrived, or after 1.5 s if none does (an empty frame, the summary view, a grid that never fetches).
  • It asks again for each reply that leaves the stats pending. c2's StatsChannel gives no reply hook, so a reply is read off the model: a new df_data_dict under the same df_meta is a stats_update that is not final, while a full frame for the same state replaces both and is not taken for one. A status other than pending ends the chain.
  • A state change is a change to post_processing, cleaning_method or quick_command_args in buckaroo_state (the fields the server reruns the dataflow for), or a frame with a new stats_gen. Either one cancels what is scheduled and waits out a delay of twice the last request's time (floor 200 ms, ceiling 3000 ms, 250 ms assumed before one is observed), so a burst of changes asks once, for the state it ends on. The first state a model holds asks without the delay. A search_string-only change, and any other field, leaves the schedule alone.
  • The model is read in a microtask after a change event, not in the handler: a frame fires one event per key in the order the server wrote them (df_data_dict before df_meta), so reading at the first event would see the new dict with the old gen.
  • Nothing is requested unless df_meta.stats says pending.

stats_gen is the server's token, and StatsChannel already drops replies for another gen, so no client-side token is kept. The scheduler does not send state_change: save_changes() already sends buckaroo_state_change.

Wiring. WebSocketModel starts the scheduler (model.scheduler), next to model.stats, so BuckarooServerView and the standalone page get it with no wiring of their own. requestStats(model, {force?}) sends the request for the gen the model shows; the scheduler and the control both use it. StateOrchestrator and requestStats are exported for a host that supplies its own IModel.

States (plan 1 section 4.0). A session that reports df_meta.stats gets a fixed-width stats column in the status bar, after the summary-view selector. It is one line in every status: "Computing summary stats…" with the pulsing dot, a "Compute summary stats" button, "Stats error: ", or "Summary stats ready". The button calls on_compute_stats, which BuckarooView and the standalone BuckarooApp pass to BuckarooInfiniteWidget and which sends stats_request {force: true}. Without that prop (Jupyter) the cell shows "Summary stats not computed" and no button. Sessions with no df_meta.stats get no column. The pinned area follows c0a: placeholders while pending, and now no row for a valueless key in the error state either, as in not_computed.

What changes

  • src/server/StateOrchestrator.ts (rewritten): StateOrchestrator, requestStats, touchesDataflow, DATAFLOW_STATE_FIELDS.
  • src/server/WebSocketModel.ts (the field and start()), src/index.ts (two exports).
  • src/components/StatusBar.tsx (StatsStatusCell, the column, the context entry), BuckarooWidgetInfinite.tsx (on_compute_stats), DFViewerParts/gridUtils.ts (error omits valueless pinned keys), src/style/dcf-npm.css.
  • src/server/BuckarooView.tsx and packages/js/standalone.tsx: the callback for the control.

Tests

Two tests-only commits (dd6ebe6a, d6afbf98), each pushed and watched on CI before the fix (95dcceec). On both, all 28 entries in the rollup completed and exactly three failed: JS / Build + Test, Server Playwright Tests and Storybook Playwright Tests; every Python / Test job passed. On the fix commit all 28 entries completed: 27 succeeded (JS / Build + Test, Server Playwright Tests, Storybook Playwright Tests and all nine Python / Test jobs among them, with the Read the Docs status) and deploy was skipped. CI logs were not read (REST rate limit); the reasons are from running the same commits locally, where each new test fails on an assertion against the stub StateOrchestrator.ts and StatusBar.tsx of the first commit. The second commit holds cases I found untested or mis-specified after the first push, written against the same stubs (the layout spec first asserted that the grid does not move at all between statuses, which is false when pinned rows are omitted).

  • src/server/StateOrchestrator.test.ts (rewritten, 45 cases): drives a fake model the way WebSocketModel drives a real one, plus a block that builds a real WebSocketModel from a fake socket. Covers nothing for a session with no df_meta.stats or a status other than pending; the first request after the first infinite_resp and after the timeout; no force flag; one request per reply until final or an error; a full frame for the same state is not a reply (both key orders); the old gen's late reply followed by nothing; each dataflow field cancelling a request waiting out its delay; search_string, df_display, show_commands and sampled changes skipped; the delay (floor, ceiling, measured once, after a completed state, for the first change of a model that started complete); resuming for the same state when no frame answers a change; requestStats; touchesDataflow; start and stop.
  • src/components/StatusBar.stats.test.tsx (new, AG Grid stubbed to capture its props): no column and no row field without df_meta.stats; a fixed-width column after df_display with it; the callback in the grid context; the cell in each status.
  • src/server/BuckarooView.stats.test.tsx (new): the callback sends stats_request {force: true} for the gen on screen, and nothing without a gen.
  • Existing files, additions only: BuckarooInfiniteWidget.flash.test.tsx (the callback reaches the status bar; error omits valueless pinned keys), gridUtils.test.ts (the same for extractPinnedRows).
  • pw-tests/stats-scheduler-states.spec.ts and StatsSchedulerStates.stories.tsx (Storybook, added to scripts/test_playwright_storybook.sh): the four statuses with their text, pinned rows and no control outside not_computed; nothing above or beside the grid moves between statuses; the control's request.
  • pw-tests/server.spec.ts (server Playwright, additions to "WebSocket data flow"): the real standalone page against a real session whose first frame is rewritten to say the stats are pending, with the stats_request answered by the test using the real server's own all_stats envelope as the payload (no session the CI server can build is deferred, since that needs xorq): the request comes after the first infinite_resp, once, with the first frame's gen, and the pinned rows fill in; a not_computed frame: pinned rows omitted, nothing asked for in 2.5 s, then the button sends the forced request; a default session never sends a stats_request.

The existing StateOrchestrator.test.ts is replaced, not extended. It asserted state_change, compute_stat_group and stat_group_result, a per-scope token and a send(string) transport, none of which the server handles or the client now has. Cases for the parts that remain (the delay's floor, ceiling and baseline, stop) are carried over in the new file. No other existing test was edited.

Run locally on the fix: jest 417 passed (365 before this phase); tsc -b; all five Storybook Playwright files; the five server Playwright files, 48 passed against a rebuilt standalone.js; tests/unit 1185 passed, 5 skipped (no Python changes). Twenty-four deliberate regressions of the implementation were tried (no paint fallback, a reply read from any dict change, the read not deferred to a microtask, every or no state change counted as a dataflow change, no delay for a later state or a delay for the first, no clamp, no restart on a state change, a request's time measured more than once or never, the first state read at start not marked, force always set, stop leaving a listener, the scheduler not started, the status bar column present or absent in the wrong cases, the callback dropped at each of three hops or called with the click event, the error text without its reason, error keeping undefined pinned rows), and each makes at least one test fail. Four more were dropped when the code they changed turned out to be dead and was removed.

I also ran the built standalone page against a scratch checkout of #1024 with a real deferred /load_expr session (a xorq memtable). The sequence was initial_state (pending, gen 1), infinite_request, infinite_resp, stats_request gen 1 five milliseconds later, stats_update (final), the status bar reading "Summary stats ready" and the pinned rows filled in. Three searches 60 ms apart then produced three pending frames (gens 2 to 4) and one stats_request, for gen 4, 200 ms after the last frame. A stats_tier: "schema" session showed the button, and clicking it sent stats_request {force: true}, which #1024 answers with stats_aborted not_requestable, leaving the status as it was. A default xorq session sent no stats_request. That check is not committed because it needs the other branch's server.

server-buckaroo-search.spec.ts ("searching filters the table data, not just the status bar count") has a race of its own: it reads the grid text as soon as the first .ag-cell is visible, and the pinned cells can be there a few milliseconds before the first data cell. In an interleaved run of 14 cold-server runs each it failed 3 times on this branch's bundle and 2 times on the bundle of the tests-only commit, whose behaviour for a default session is c2's. The job retries twice on CI. It is not changed here.

Why default behaviour is unchanged

A session whose server reports no df_meta.stats has an idle scheduler: it registers four listeners and arms no timer. The status bar has no stats column. extractPinnedRows changes only for status error, which no server sends today. The server Playwright test above asserts that a default session sends no stats_request. A session that is pending exists only with a deferred /load_expr, which is opt-in.

Deviations from the plan

  • StateOrchestrator takes an IModel; there is no separate adapter. The plan's adapter existed to bridge send(string) to IModel.send(object), and the class now sends objects through the model directly.
  • Names are aligned by removal: the per-scope token, state_change, compute_stat_group and stat_group_result are gone. The plan's "a merge" is c2's StatsChannel; the scheduler only sees its effect on the model.
  • The scheduler is started by WebSocketModel, not by BuckarooView and standalone.tsx separately, so both paths share one construction site. A host that gives BuckarooView its own IModel runs new StateOrchestrator({model}).start() itself. The two wiring copies carry only the callback for the control.
  • The first rows are signalled by msg:custom for an infinite_resp (the model emits it once it has paired the response with its parquet frame), not by the grid reporting a paint, with a 1.5 s timeout.
  • No back-off while the user scrolls: today's request is a whole run and the client has no scroll signal to take. The delay after a state change covers searching and clicking.
  • The summary view's label for not-computed stats is the status bar's stats column, which shows in every view, not text inside the summary grid.
  • error omits valueless pinned keys; c0a left it behaving as complete.
  • Layout: placeholders keep the pinned area at its height until the values arrive, so pending to complete moves nothing. not_computed and error omit the rows, per section 4.0, so the grid is shorter by the pinned area there, and would grow by it if a forced request completes. The status bar and the grid's position and width never change.
  • Two tests-only commits instead of one (see Tests).
  • On today's server (feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024) the button's forced request is answered stats_aborted not_requestable, so it has no effect until plan 3 lets force run.

Not in this PR

  • The policy fields (auto_request, requestable, demand_columns, ceiling) and the tier-aware control: plan 3 phase 5. The button is shown for every not_computed session whether or not the server could compute.
  • Any server change.
  • A buckaroo-js-core release and the tallyman bump. Tallyman pins buckaroo-js-core 0.15.8, so a host gets the scheduler only after both, and should not select stats_delivery: "deferred" before then.

Stack

Built on feat/rowsfirst-c2-client-stats-channel (#1025), which itself contains c0a (#1020), so the diff against main includes their commits until they merge: c0a 29e1e42c (tests) and 405f4ded (fix), c2 fe046acc, 3118435a, 1fd9cff8 (tests) and 5935432d (fix). This phase's commits are dd6ebe6a and d6afbf98 (tests) and 95dcceec (fix). Read only those. The server branch #1024 is not part of this PR.

🤖 Generated with Claude Code

paddymul and others added 7 commits October 3, 2026 22:58
…protocol (rows-first c0a)

Tests for the client half of plan 1 phase 0a. A server that sends the
summary stats separately, or not at all, leaves the first message
without values the client has been assuming.

BuckarooView: a change that reached the model before the effect
subscribed, one initial_state with metadata decoding once, and
out-of-order decodes applying the newer.

Pinned rows: a valueless key shows a placeholder with its own row id
while df_meta.stats.status is pending, and is omitted when not_computed.
The simple tooltip returns nothing for a valueless cell. color_map is
silent without bins and restyles when they arrive. An unrelated
df_data_dict update keeps the in-flight indicator when the server
reports df_meta.stats.

One Storybook Playwright test covers pending, not_computed and complete
in a real browser, and is added to the Storybook list in
scripts/test_playwright_storybook.sh.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… c0a)

BuckarooView reads model.get() for every key once its listeners are in
place, so a change:* emitted before the effect ran reaches React. All
df_data_dict values go through one decoder (makeLatestDictDecoder) that
skips a dict it has already seen and applies only the newest decode, so
an initial_state with metadata decodes once and out-of-order decodes
cannot overwrite a newer one. standalone.tsx gets the same decoder and
catch-up.

df_meta.stats.status reaches the grid as stats_status. While pending, a
required pinned key with no value becomes a placeholder row that carries
its key as index (unique row id, empty value cells). When not_computed
it is omitted. A missing df_meta.stats, or any other status, keeps the
old behaviour.

The simple tooltip returns nothing for a cell with no value or no row
data. color_map no longer logs when bins are missing, and the grid
refreshes the color-mapped columns when their bins change after the first
render. That needs RenderApiModule, which was not registered, so
api.refreshCells logged AG Grid error 200 and did nothing.

BuckarooInfiniteWidget keeps inFlight when only df_data_dict changed and
the server reports df_meta.stats; the answer to the dispatched change is
a frame with a new df_meta. Without df_meta.stats the rule is unchanged.

The Playwright story test no longer hovers a valueless pinned cell: with
the old tooltip it still passed, since AG Grid does not call the tooltip
for an empty value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Jest tests, driven through a WebSocketModel with a fake socket, for the
client half of the stats wire: a stats_update is key-merged into all_stats
and dropped when its stats_gen is not the expected one, the expected gen
follows df_meta.stats.gen on every applied initial_state, a merge in
flight is discarded or redone when a frame replaces the dict, and
stats_aborted moves the status. Plus withStatsCapability and
BuckarooServerView putting ?caps=stats_update on the WebSocket URL, and a
server Playwright test that the standalone page does the same.

StatsChannel.ts is a stub (identity withStatsCapability) so the tests fail
on assertions rather than on a missing module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ct (rows-first c2)

Three more cases for the stats_update merge, found untested after the
first test commit: a wide parquet_b64 payload as the server sends it (the
shared summary_stats fixture, not a json envelope), a model that holds no
df_data_dict yet, and a dict with no all_stats key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two stats_update messages for one gen are applied in arrival order even
when the first payload decodes more slowly: the final update merges last
and the status completes only after both merges.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ability (rows-first c2)

StatsChannel is the client half of the stats wire. WebSocketModel hands it
every text frame first. A stats_update for the gen the model's df_meta
reports is decoded and key-merged into df_data_dict.all_stats (new row
objects, a new dict), updates apply one at a time in arrival order, and a
final update sets df_meta.stats to complete at the update's tier. A merge
is dropped when the gen moves on while it decodes and redone when a frame
replaces the dict. stats_aborted for the expected gen sets error or
not_computed. Other message types are ignored as before.

withStatsCapability adds caps=stats_update to a WebSocket URL; both wiring
copies (BuckarooServerView and the standalone page) use it so the server
treats them as capable clients.

The guard tests that already pass before the fix (caps already present, no
df_meta.stats, ignored aborts, unknown types, infinite_resp pairing) are
added here, with a makeModel(null) helper so a model without stats can be
built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ows-first c4)

Jest tests for the scheduler (StateOrchestrator rewritten around an IModel:
a stats_request after the first infinite_resp, one request per reply until
final, nothing after a state change, nothing for a search_string-only change),
for the status bar's stats column and Compute summary stats control, for the
widget and BuckarooView wiring of that control, and for pinned rows in the
error state. Playwright tests on Storybook cover the four states without
layout shift and the control's request, and the server spec runs the real
standalone page against a session whose first frame says the stats are pending.

StateOrchestrator.ts, StatusBar.tsx and BuckarooWidgetInfinite.tsx carry only
the signatures the tests use, so the tests fail on assertions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

📦 TestPyPI package published

pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37187295169

or with uv:

uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37187295169

MCP server for Claude Code

claude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37187295169" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table

📖 Docs preview

🎨 Storybook preview

…tus layout and the control wiring (rows-first c4)

Cases found untested or mis-specified after the first push, written against
the same stubs: a model that starts complete waits out the delay for its first
state change, as does a change made after the earlier stats completed; a
request's time is measured once; the Compute summary stats button calls its
handler with no arguments; the standalone page offers that button for a session
whose stats are not computed and asks for nothing until it is clicked. The
layout spec now says what holds across statuses (nothing above or beside the
grid moves, and the grid's height follows the pinned area), and the story loads
the widget's stylesheet as the real page does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g, not computed and error states (rows-first c4)

StateOrchestrator becomes the client scheduler for the stats wire. It takes an
IModel, so it sends stats_request {stats_gen, scope: "raw"} through model.send
and watches change:df_meta, change:df_data_dict, change:buckaroo_state and
msg:custom. It asks once the first infinite_resp has arrived (or after 1.5 s
without one), asks again for each reply that leaves df_meta.stats pending, and
stops when the status changes. A change to post_processing, cleaning_method or
quick_command_args waits out a delay of twice the last request's time (200 to
3000 ms) before asking for the next gen; a search_string-only change, or any
other field, leaves the schedule alone. Nothing is requested unless
df_meta.stats says pending.

WebSocketModel starts the scheduler, so BuckarooServerView and the standalone
page get it with no wiring of their own. The status bar gets a fixed-width stats
column, for sessions that report df_meta.stats, showing loading, a Compute
summary stats button that sends stats_request {force: true}, the error reason, or
ready. Valueless pinned rows are omitted in the error state, as in not_computed.
requestStats and StateOrchestrator are exported for hosts with their own IModel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@paddymul

paddymul commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Closing. Under the revised D2 of ADR-002 (#1043, 7943412) the server pushes stats after the first row reply, so the client scheduler that sends stats_request is not needed.

The status-bar stats cell, the on_compute_stats wiring and the error-state pinned-row rule still apply. They are the parts to carry into a client UI PR on top of #1025. The branch is kept for them.

@paddymul paddymul closed this Oct 6, 2026
paddymul added a commit that referenced this pull request Oct 6, 2026
…/load_expr (rows-first s3)

The page renders its rows and the schema-tier dtype row, and the stats rows stay empty: no client sends stats_request after the first frame (the scheduler PR #1027 is closed) and the server pushes nothing (ADR-002 D2 is unimplemented).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
paddymul added a commit that referenced this pull request Oct 6, 2026
…ed sessions (rows-first s3) (#1024)

* test(server): failing tests for the stats wire format and whole-run stats_request (rows-first s3)

On a deferred /load_expr session a stats_request {stats_gen, scope} should
return a stats_update with the matching stats_gen whose inline wide payload
equals the all_stats an inline session sends, a stale stats_gen should get
stats_aborted and run no query, and /load_expr and /reload_expr should bump the
generation. df_meta.stats should be injected on every frame and survive a
dataflow-field change, which returns the session to the schema tier. With a
caps client and a legacy client on one session, the legacy client should keep
getting complete messages through the websocket broadcast, the /load_expr,
/reload_expr, /load and /load_compare pushes and the highlight overlay, while
the caps client gets a stats-free frame and then pulls a stats_update. A spy
telemetry sink should see a stats.request span.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing tests for stats reuse, component_config and generation edge cases (rows-first s3)

Four more cases for the stats wire format, kept in their own commit so each is
seen failing on CI before the implementation lands. Returning to a state whose
stats were completed once is answered from summary_stats_cache with no query.
Completing the stats keeps the session's component_config on the refreshed
display config. A warm /load_expr, which rebuilds nothing, leaves stats_gen
alone. A stats_request on a session with no data is answered with
stats_aborted.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3)

A client that advertises ?caps=stats_update gets a stats-free initial_state on a
deferred /load_expr session (df_meta.stats.status "pending") and pulls the stats
with stats_request {stats_gen, scope}. The reply is a stats_update carrying the
dataflow's all_stats as an inline wide envelope, or stats_aborted when the
generation is stale. The request is the whole run: one synchronous call that
computes the full stats, writes the full-tier summary_stats_cache entry, assigns
summary_sd and refreshes the session snapshot through one helper.

stats_gen is a server-owned counter bumped by every load handler and by a state
change that touches a dataflow field, which also returns a deferred session to
the schema tier. df_meta.stats is injected by build_state_message from the
session, since the dataflow rebuilds df_meta wholesale. Every send site goes
through build_state_message_for, so a client without the capability still gets
complete messages (its missing stats run synchronously first); broadcast_state
replaces the five copies of the send loop and sends to capable clients first.
The stats_request branch binds the session's telemetry sink and emits a
stats.request span.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing tests for errs on a stats cache hit, the summary dedupe key, span sinks and a failed state change (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(server): set_stats_tier keeps errs per cache entry and rolls back; stats.complete span; failed state change restores the tier (rows-first s3)

- A summary_stats_cache hit no longer reports a clean run: the errs of the run that filled an entry are kept beside it (_summary_errs_cache) and set_stats_tier reads them back. errs is assigned before summary_sd so _populate_sd_cache files them with the sd.
- set_stats_tier puts the tier and the summary key back when the switch raises.
- complete_stats times the run as stats.complete rather than firstpull.stats_total, which also fired for completions long after the load, and binds the session's telemetry sink only when it has one so an outer sink is not replaced by None.
- A buckaroo_state_change that raises puts the dataflow's tier back, so it matches the snapshot the session still describes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): the stats wire tests follow main's send path and the stats.complete span (rows-first s3)

The legacy overlay test calls _send_client_state, which replaced _send_highlight_overlay on main, the span test expects stats.complete, and the sink test only rules out firstpull.stats_total (the xorq dataflow emits its own firstpull.summary_stats).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): e2e check that eager stats reach the DOM on /load_expr (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing e2e check that deferred stats reach the DOM on /load_expr (rows-first s3)

The page renders its rows and the schema-tier dtype row, and the stats rows stay empty: no client sends stats_request after the first frame (the scheduler PR #1027 is closed) and the server pushes nothing (ADR-002 D2 is unimplemented).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing tests that deferred stats are pushed after the first row reply (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(server): push stats after the first row reply on a deferred session (rows-first s3, ADR-002 D2)

A client that advertises ?caps=stats_update and is sent a pending frame is owed
that generation's stats. The handler registers a continuation on the write
future of the client's first row reply; it sends the stats_update once the rows
are on the socket, if the connection is still open and the generation is still
current. The stats run once per session, so a later connection's push is
answered from the session. The stats.push span records the gap since the rows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
testpypi — 95dcceec Deployed Oct 4, 2026 by paddymul via Publish to TestPyPI #1692
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