Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/external-sort-authority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@pretable/core": minor
"@pretable/react": minor
---

`processing: { sort: "external" }` now suppresses local sorting, the way
`filter: "external"` suppresses local filtering since #447.

It previously suppressed nothing: the declaration was read in two advisory
places and the engine went on applying `query.sort`. That left the consumer who
declared it worse off than one who did not — declaring external sort authority
silences the partial-window warning and unlocks the full population as
`aria-rowcount`, while the local re-sort it silences the warning about kept
running.

Suppression changes what is APPLIED, never what is REPORTED: `aria-sort`,
`onQueryChange` and the snapshot's `query` are untouched. A consumer holding a
complete window who legitimately sorts locally is unaffected — they declare
`"engine"`, which is the default.
2 changes: 1 addition & 1 deletion apps/website/content/docs/grid/pretable-surface.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ See [Row grouping and aggregation](/docs/grid/grouping) for typed grouping, aggr

## Server-applied filtering and sorting

`processing` declares whether filter and sort work was performed by the local engine or an external authority; `resultMeta` then describes the result those rows came from, and `dataState` says where its request stands. The two slices differ in what the declaration does. `filter: "external"` is acted on in rows mode: the surface stops the engine applying `query.filters` while continuing to publish them, so the funnel, `onQueryChange`, and `aria-sort` are unchanged. `sort: "external"` is a claim about the rows you supplied and suppresses nothing — the engine still orders what it holds. All of it — query ownership, totals, the lifecycle phases, and `renderBodyState` — is documented against a live endpoint in [Server-side data](/docs/server-data).
`processing` declares whether filter and sort work was performed by the local engine or an external authority; `resultMeta` then describes the result those rows came from, and `dataState` says where its request stands. Both are acted on in rows mode: the surface stops the engine applying `query.filters` and `query.sort` while continuing to publish them, so the funnel, `onQueryChange`, and `aria-sort` are unchanged. All of it — query ownership, totals, the lifecycle phases, and `renderBodyState` — is documented against a live endpoint in [Server-side data](/docs/server-data).

## Telemetry

Expand Down
22 changes: 11 additions & 11 deletions apps/website/content/docs/server-data/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ Nothing about that is a different component. It is `<PretableSurface>` with four

External processing moves less than people expect. The reader's intent, the interaction state, and the geometry stay in the grid; the data itself becomes yours.

| Concern | Owner | Notes |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Query intent | grid | funnels, header clicks, and the group panel still produce filters, sort, and `rowGroups` — you receive them |
| Focus, selection, editing | grid | keyboard, marquee, and cell editors work identically against server-supplied rows |
| Viewport geometry | grid | row virtualization, column layout, pinning, and resizing never consult where the rows came from |
| Fetching | consumer | the grid issues no requests; nothing in it knows a network exists |
| Choosing the records | consumer | `processing.filter: "external"` declares that the server, not the engine, decided which records exist — and the engine stops re-selecting them |
| Choosing the order | consumer | `processing.sort: "external"` says the same about order but suppresses nothing; leaving it to the engine over a partial window sorts a sample |
| Totals | consumer | the row count is whatever `resultMeta.total` claims, and how sure you are of it is part of the claim |
| Lifecycle | consumer | `dataState` is never inferred and has no default — loading, staleness, and failure are things you declare |
| Concern | Owner | Notes |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Query intent | grid | funnels, header clicks, and the group panel still produce filters, sort, and `rowGroups` — you receive them |
| Focus, selection, editing | grid | keyboard, marquee, and cell editors work identically against server-supplied rows |
| Viewport geometry | grid | row virtualization, column layout, pinning, and resizing never consult where the rows came from |
| Fetching | consumer | the grid issues no requests; nothing in it knows a network exists |
| Choosing the records | consumer | `processing.filter: "external"` declares that the server, not the engine, decided which records exist — and the engine stops re-selecting them |
| Choosing the order | consumer | `processing.sort: "external"` says the same about order, and is acted on the same way; leaving it to the engine over a partial window sorts a sample |
| Totals | consumer | the row count is whatever `resultMeta.total` claims, and how sure you are of it is part of the claim |
| Lifecycle | consumer | `dataState` is never inferred and has no default — loading, staleness, and failure are things you declare |

Two things about `processing` are worth stating plainly, because "external" reaches further than one slice and less far than the other.

`filter: "external"` **stops the engine selecting records, without changing what it reports**. The published filters stay published — the funnel still shows them, `onQueryChange` still hands them to you — and the engine stops re-applying them to the rows you brought back, because you already did. That matters exactly when the rows and the query disagree, which the lifecycle deliberately allows: while a new result loads, the previous one is still on screen answering the previous query. In the example above, a request that fails leaves the previous rows in place and leaves them readable — filter **Customer** for `fail` and the body keeps every row it already had, with an error strip above it, the same as `notContains` `fail`. Sort is not part of that bargain: `sort: "external"` is a claim about the order you supplied, and the engine still orders what it is given.
`filter: "external"` **stops the engine selecting records, without changing what it reports**. The published filters stay published — the funnel still shows them, `onQueryChange` still hands them to you — and the engine stops re-applying them to the rows you brought back, because you already did. That matters exactly when the rows and the query disagree, which the lifecycle deliberately allows: while a new result loads, the previous one is still on screen answering the previous query. In the example above, a request that fails leaves the previous rows in place and leaves them readable — filter **Customer** for `fail` and the body keeps every row it already had, with an error strip above it, the same as `notContains` `fail`. `sort: "external"` is the same bargain one axis over: the sort stays published and the engine stops re-applying it, so a window the server ranked keeps that ranking instead of being re-sorted as a sample.

What the claim does buy is honesty about counts, and it cuts both ways. With both slices external and an exact total, `aria-rowcount` may publish the whole population instead of just the rows in the model, because loaded position and dataset position finally line up. In the other direction, declaring external filtering narrows what a select-all or a CSV export is allowed to call "all rows": unless the exact total says you already hold every matching record, the answer is the loaded ones. That is [Totals and honesty](/docs/server-data/totals).

Expand Down
4 changes: 2 additions & 2 deletions apps/website/content/docs/server-data/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The search below sends one POST to `/api/docs/rows` per submit and waits the end

Most of those distinctions are ones only you can draw. `stale` and `refreshing` differ by whether the query changed; `loading` and `stale` differ by whether a result has ever committed. The grid witnessed neither event. One flag is usually enough for both — the example keeps a `hasCommitted` ref and reads it once per search.

One thing has to be true for `stale` to be worth declaring, and it is a claim you make on another page. The previous rows only stay readable if nothing re-selects them while they are there — and where the reader's filters live in the grid's own `query`, a funnel or a header, `stale` is precisely the moment the engine holds a filter the loaded rows have never answered. [`processing: { filter: "external" }`](/docs/server-data/query-ownership#what-external-filtering-suppresses) is what stops it applying that filter to them; short of it the body empties and refills on every search, which is the flicker this phase exists to prevent. The example above drives its search from a form outside the grid's query, so there is nothing there for the engine to re-apply — but [the overview's grid](/docs/server-data) filters through the funnel and depends on the declaration.
One thing has to be true for `stale` to be worth declaring, and it is a claim you make on another page. The previous rows only stay readable if nothing re-selects them while they are there — and where the reader's filters live in the grid's own `query`, a funnel or a header, `stale` is precisely the moment the engine holds a filter the loaded rows have never answered. [`processing: { filter: "external" }`](/docs/server-data/query-ownership#what-external-authority-suppresses) is what stops it applying that filter to them; short of it the body empties and refills on every search, which is the flicker this phase exists to prevent. The example above drives its search from a form outside the grid's query, so there is nothing there for the engine to re-apply — but [the overview's grid](/docs/server-data) filters through the funnel and depends on the declaration.

What the surface actually draws depends on the phase **and** on whether the body currently has rows to show:

Expand All @@ -52,7 +52,7 @@ Nothing here is inferable after the fact, either. An empty `rows` array is a res

The choice keys on what the body is currently rendering rather than on how many records are loaded, which matters under [engine filter authority](/docs/server-data/query-ownership): a grid can hold plenty of records and still show nothing, and that case wants the block. Under external filter authority — the remote shape this rule was written for — the two counts are the same number.

The half of this you own is the same discipline on your side: when a request rejects, leave the last good rows in state. The example's failure handler sets the phase and nothing else — rows, total, and dataset key are all left exactly as the last successful search committed them, which is why its header is still sortable while the error strip is up. Clear the rows first and then declare `error` and you have thrown away a result the reader could still have used, and traded the strip for the full-viewport block.
The half of this you own is the same discipline on your side: when a request rejects, leave the last good rows in state. The example's failure handler sets the phase and nothing else — rows, total, and dataset key are all left exactly as the last successful search committed them, which is why its header stays interactive while the error strip is up — a click still publishes the new sort through `onQueryChange`, though under external sort authority the rows on screen keep the order the last successful response gave them until a request succeeds. Clear the rows first and then declare `error` and you have thrown away a result the reader could still have used, and traded the strip for the full-viewport block.

The strip carries no live-region role of its own — the surface keeps exactly one live region, and the strip does not become a second one.

Expand Down
Loading
Loading