Repository navigation
feat(client): scheduler and control request incremental stats with a columns hint (rows-first c4b) - #1030
Closed
paddymul wants to merge 12 commits into
Closed
feat(client): scheduler and control request incremental stats with a columns hint (rows-first c4b)#1030paddymul wants to merge 12 commits into
paddymul wants to merge 12 commits into
Conversation
…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>
…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>
…umns hint (rows-first c4b) The scheduler and the Compute summary stats control should send stats_request with incremental: true and, when the grid has reported them, the columns it shows as the columns hint. A run is one request per partial reply, with the stats pending until the final reply, and a gen change mid-run stops it and starts one for the new gen. Jest: the scheduler's request shape, hint and chain against a fake model and against a real WebSocketModel (partial replies, a final that carries the complete all_stats, a whole-run final from a server that ignores the field, a gen change mid-run), requestStats, BuckarooView's callbacks, and the grid reporting its viewport columns. Playwright: the server spec (a harness answers stats_request over a real session, including a wide table whose hint follows a horizontal scroll) and the Storybook control. The existing request assertions gain incremental: true. The two unused on_visible_columns props are stubs so the tests compile. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributor
📦 TestPyPI package publishedpip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37193367183or 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.dev37193367183MCP server for Claude Codeclaude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37193367183" --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 |
… test releases a reply (rows-first c4b) The grid reports the columns it shows a frame after it renders them, and the wide-table test released the reply as soon as the header cells had changed, so on a loaded machine the next request could go out with the earlier hint. Wait two animation frames first. No assertion changes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ith a columns hint (rows-first c4b) requestStats, which the scheduler and the Compute summary stats control both use, now sends stats_request with incremental: true and, when the grid has reported them, the columns it shows as the columns hint, read from the model's visible_columns key when the request goes out. A server on the unit path answers with a partial stats_update per request, the scheduler asks again for each, StatsChannel merges them without leaving "pending", and the final reply with the complete all_stats ends the run. A server that ignores the fields answers with a whole-run final, which ends it the same way. DFViewerInfinite takes on_visible_columns and reports the data columns that have a header cell in the grid's viewport on grid ready and on onVirtualColumnsChanged. BuckarooInfiniteWidget forwards it, and BuckarooView and the standalone page store the list on the model with setVisibleColumns. The columns are read from the rendered header cells because the grid's column API is not registered, and registering it would switch on the column-state calls that are inert today. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 4, 2026
paddymul
changed the base branch from
main
to
adr-002-rows-first-stats-delivery
October 6, 2026 14:40
Collaborator
Author
This branch was successfully deployed
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.
Problem
The server phases s4 and s5 (#1026, #1028) added stat units and an opt-in
incremental: trueonstats_request: such a request runs units for about 75 ms and answersstats_update {final: false, remaining, payload}with the new fragments, the final reply carries the completeall_stats, and acolumnshint (the grid's rewritten column names) runs the units that cover those columns first. The client scheduler of c4 (#1027) was written in parallel and still sends whole-run requests: a request with noincrementalfield runs every unit in one synchronous call, however long the longest unit is, and the hint is never sent. The Compute summary stats control sends a whole-run request too.Phase and plan references
Client phase c4b, the follow-up to c4. Plan 1 (
plans/01-rows-first-stats-separate-plumbing.md) section 9 "Phase 4" and section 5 (Strategy C), with section 3 and section 4.0. Plan 2 (plans/02-rows-first-xorq-and-lazy-polars.md) section 3 and section 4.1 ("Request"). Message names and shapes follow the server branches #1024 and #1028.Approach
Request shape.
requestStatsinStateOrchestrator.tsis the one place that builds astats_request, and the scheduler and the control both use it. It now sends{type, stats_gen, scope: "raw", incremental: true}, pluscolumnswhen the grid has reported the columns it shows (an empty or missing list sends no hint), plusforce: truefor the control. The hint is read from the model'svisible_columnskey at the moment a request goes out, so each request of a run carries the columns on screen then.The run. The scheduler's chain was already one request per reply that leaves the status pending, read off the model (a new
df_data_dictunder the samedf_meta). It needed no new logic for partial replies: c2'sStatsChannelmerges a non-finalstats_updateintoall_statsand leavesdf_metaalone, so the status stayspendingand the placeholder pinned rows stay until the final reply, which carries the completeall_statsand sets the status tocomplete. A gen change (a local dataflow-field change or a frame with a new gen) cancels the run and starts a new one after the delay, andStatsChanneldrops a late reply for the old gen. A server that does not know the fields ignores them, runs the whole run and answersfinal: true, which ends the run the same way. Tests pin each of these.Where the hint comes from.
DFViewerInfinitetakeson_visible_columnsand calls it with the data columns that have a header cell in the grid's viewport, when the grid is ready and ononVirtualColumnsChanged, skipping an unchanged list.BuckarooInfiniteWidgetforwards the prop.BuckarooViewand the standaloneBuckarooApppass a callback that stores the list on the model withsetVisibleColumns(model, columns)(new, exported next torequestStats). The key is client-side: the server never sends it, andWebSocketModelkeeps it acrossinitial_stateframes.The columns are read from the grid's rendered header cells (
.ag-header-viewport .ag-header-cell[col-id], one frame after the event), not fromapi.getAllDisplayedVirtualColumns. That function lives in AG Grid'sColumnApiModule, which this package does not register. On mainapi.getColumn(the active-cell refresh inonCellClicked) andapi.getColumnStateandapplyColumnState(the per-view column state in theview_nameeffect) are already inert for the same reason: AG Grid logs error #200 for each. Registering the module would switch them on in every grid, which changes default behaviour, so this PR does not. I did not touch those call sites.What changes
src/server/StateOrchestrator.ts:requestStatssendsincremental: trueand the hint;VISIBLE_COLUMNS_KEYandsetVisibleColumns; the header comment describes the run.src/components/DFViewerParts/DFViewerInfinite.tsx:on_visible_columns(the header-cell read,onGridReadyandonVirtualColumnsChanged).src/components/BuckarooWidgetInfinite.tsx: forwardson_visible_columnsin the buckaroo widget (viewer mode has no stats).src/server/BuckarooView.tsx,packages/js/standalone.tsx: the callback.src/index.ts: exportssetVisibleColumns(named and on the defaultsrtobject).Tests
All new tests are in existing files of the same kind. The tests-only commit is
d7eda002, pushed and watched on CI before the fix. A second tests-only commit (5d1e0467) adds a two-frame wait to the wide-table test, which failed once in a full local run: it released the reply as soon as the header cells had changed, a frame before the page reports its columns. No assertion changed. The fix ise5649605.CI on
d7eda002: all 28 entries in the rollup completed and exactly three failed,JS / Build + Test,Storybook Playwright TestsandServer Playwright Tests; everyPython / Testjob passed (this phase has no Python change). CI logs were not read (REST rate limit); the reasons are from running the same files locally against the stubs of that commit, where 44 jest tests failed on assertions, five of the new server Playwright tests failed and the Storybook control test failed.src/server/StateOrchestrator.test.ts: a block "incremental requests" (the shape with no hint, with a hint, with an empty list, the hint re-read for each request of a run, the same shape for the first request of the next gen);requestStatswith the hint and force; in the block that builds a realWebSocketModelfrom a fake socket: partial replies walked to the final one withdf_metaunchanged (the same object) until the last, a whole-run final from a server that ignores the field, a gen change mid-run (late reply for the old gen dropped, one request for the new gen, which then chains).src/components/DFViewerParts/DFViewerInfinite.test.tsx: the grid reports the data columns of its viewport (not the index column), again when they change, not when they stay the same, and[]before layout. The AG Grid mock now renders header cells for the columns it is told are in view.BuckarooInfiniteWidget.flash.test.tsx: the widget hands the grid the callback.BuckarooView.stats.test.tsx: the callback stores the list and the control's request carries it.pw-tests/server.spec.ts(server Playwright, "WebSocket data flow"): the real standalone page against a real session whose first frame is rewritten to pending, with a harness answeringstats_request(the real server's ownall_statsas the final payload, so no xorq is needed): several partial replies lead to repeated requests and the status stayspendinguntil the final; a gen change mid-run stops the run for the old gen and starts one for the new gen; a 60-column table sends only the columns on screen, and after a horizontal scroll the next request carries the new ones.pw-tests/stats-scheduler-states.spec.ts(Storybook): the control's request carriesincremental,forceand the columns the grid reports; a stat row that has arrived for one column leaves the other columns' cells empty while the status ispending. The second one passes on the earlier code, since AG Grid does not render a cell whose value is absent. It is a characterization test of what a partial merge shows.Existing tests changed, and why. The scheduler's
request()helper inStateOrchestrator.test.tsgainedincremental: true, and the existing assertions on the sent message inBuckarooView.stats.test.tsx,server.spec.ts(two) andstats-scheduler-states.spec.ts(one) gainedincremental: trueand the hint where the grid reports one: the request shape changes by design, and every one of those asserts the exact message. No assertion about behaviour was loosened.Two
on_visible_columnsprops are unused stubs in the tests-only commit so that the new tests compile and fail on assertions.Run locally on the fix: jest 431 passed (417 before this phase);
tsc -b; the five Storybook Playwright files, 16 passed; the server Playwright files, 42 passed inserver.spec.tson three runs in a row and 17 in the other four files on three runs in a row (in one earlier full runserver-buckaroo-search.spec.tsfailed once, on the race that c4's description names, and did not fail in those three runs);tests/unit1185 passed, 5 skipped. Twelve deliberate regressions of the implementation (noincremental, no hint, an empty hint sent, another model key, the index column not filtered, no dedupe, noonVirtualColumnsChanged, no report on grid ready, the widget,BuckarooViewand the control each dropping their hop, the callback a no-op) each make at least one jest test fail, and dropping the callback fromstandalone.tsxmakes five server Playwright tests fail.I also ran the built standalone page against a scratch checkout of #1028 (detached at
ad26fdf4) on a free port, with a real deferred/load_exprsession over a 200,000-row, 30-column xorq memtable. The observed sequence:initial_state(pending, gen 1),infinite_request,infinite_resp, thenstats_request {gen 1, incremental: true, columns: a-g}15 ms later; 16 partialstats_updatereplies withremaining30, 28, ... 2, each answered at once by the nextstats_request(the hint widening toa-jafter the grid laid out); the 17th replyfinal: true, remaining: 0(carryingdf_display_args, which the client does not apply), and the status bar reading "Summary stats ready". The whole run took about 2.0 s, in requests of 120 to 200 ms. In a second run a search term was typed 700 ms in: the next reply for gen 1 arrived after thebuckaroo_state_changeand changed nothing, the server sentinitial_statepending gen 2, onestats_requestfor gen 2 went out 315 ms after that frame, and the run chained to a final reply. No request for gen 1 followed the change. That check is not committed because it needs xorq and the other branch's server.Why default behaviour is unchanged
Nothing is requested unless
df_meta.statssayspending, which only a deferred/load_exprsession reports, so a default session still sends nostats_request(the existing server Playwright test asserts it). The hint is reported only when a host passeson_visible_columns; the Jupyter widget and viewer mode do not, so their grids add nothing (the one new prop onDFViewerInfiniteis optional and, when absent, the read is skipped). The grid registers no new AG Grid module. A server that predatesincrementalignores the field.Deviations from the plan
visible_columnsinstead of through a setter on the scheduler, so a host that runs its own scheduler on its ownIModelreads it too andrequestStatsneeds no extra argument.not_computedsession the status is notpending, the scheduler ignores it, and feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) #1024 and feat(stats): stats_request runs units for a time budget, with per-client cursors and the final assignment (rows-first s5) #1028 answer everyforcerequeststats_aborted not_requestabletoday. How a forced run continues is plan 3's (force semantics), so I left it.df_display_args(the rebuilt config, sent by feat(stats): stats_request runs units for a time budget, with per-client cursors and the final assignment (rows-first s5) #1028 when its digest differs) is not applied by the client. The scope names no config upgrade, and plan 3 lists "an upgradeddf_display_argsneither remounts nor purges rows" as unmeasured.Not in this PR
auto_request,requestable,demand_columns,ceiling) and the tier-aware control: the next client phase.buckaroo-js-corerelease and a tallyman bump.Stack
Built on
feat/rowsfirst-c4-client-stats-scheduler(#1027), which contains c2 (#1025) and c0a (#1020). The PR targetsmain, so the diff includes their commits until they merge: c0a29e1e42cand405f4ded; c2fe046acc,3118435a,1fd9cff8and5935432d; c4dd6ebe6a,d6afbf98and95dcceec. This phase's commits ared7eda002and5d1e0467(tests) ande5649605(fix). Read only those. The server branches #1024 and #1028 are not part of this PR.🤖 Generated with Claude Code