perf(tables): stop a table write refetching every loaded page in the tab that made it - #6698
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Each tab now sends
Reviewed by Cursor Bugbot for commit 8e18553. Configure here. |
Greptile SummaryThe PR prevents an originating table tab from refetching locally reconciled single-row writes and exposes a reliable end-of-pagination signal for Query Rows.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/api/client-id.ts | Generates a per-page client identifier and hashes it before attribution is exposed to table subscribers. |
| apps/sim/lib/table/events.ts | Adds an attributed edit signal while retaining unattributed signaling for mutation paths that require refetching. |
| apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts | Skips edit invalidation only when the event fingerprint matches the current tab. |
| apps/sim/hooks/queries/tables.ts | Invalidates non-default row queries whose filtering, ordering, or counts cannot be repaired by an in-place row patch. |
| apps/sim/tools/table/query_rows.ts | Exposes a nullable pagination cursor in both transformed output and tool metadata. |
| apps/docs/content/docs/en/tables/using-in-workflows.mdx | Corrects pagination instructions to advance by received row count and terminate using nextCursor. |
Reviews (3): Last reviewed commit: "perf(tables): stop a table write refetch..." | Re-trigger Greptile
c4f94c5 to
4b94836
Compare
|
@cursor review |
4b94836 to
ee2cdd6
Compare
ee2cdd6 to
8e18553
Compare
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8e18553. Configure here.
Summary
The refetch storm
#5991added a live-collaboration signal: a row write appends aneditevent that every subscriber of the table refetches on. The stream carries no originator, so the writing client refetches its own write —rowsRootis a prefix over the infinite query, so one cell edit costs N sequentialGET /rows(up to 1000 rows each, plus aCOUNT(*)on page 0) about 250–750ms after the write.For the acting tab that work is pure duplication, and it actively undoes existing optimism:
useUpdateTableRowmerges the server row inonSuccessand deliberately never invalidates rows;useCreateTableRowavoids the rows root specifically to prevent insert flicker. The echo invalidates it anyway, a second later. On delete it's a second full refetch racing the one the hook already issued — and sinceinvalidateQueriesdefaults tocancelRefetch: true, it can cancel and restart that one.lib/table/events.tshad carried a standing TODO describing this and naming the fix. This is that fix.Writes now optionally carry the originating tab, and a client ignores its own echo. Every tab sends an opaque per-tab id (
x-sim-client-id, generated once per page load) — matching the existing pattern for cross-cutting headers (execution-deadline-header.ts,call-chain.ts,mcp/constants.ts): a small module owning the constant plus both sides' accessors.Attribution is deliberately narrow. Only three routes stamp it — single-row create, update, delete — because only their hooks reconcile the server's answer across every cached rows query. Bulk, filter-scoped, upsert, import, copilot, and run-dispatch writes stay unattributed and refetch exactly as before; suppressing those would strand the writer on stale rows. That invariant is a claim about a client hook, which nothing in the type system ties to the call site, so
events.attribution.test.tspins the allowlist — a fourth call site fails the build.Per-tab rather than per-user: a shared id would make your second tab ignore the first tab's edits.
The
nextCursorholesFallout from
#6582, which turned byte-cut pages on by default. Two in-repo consumers still assumed a full page meant more data:apps/docs/.../using-in-workflows.mdxtold users to advance Offset by the Limit — "0, then 100, then 200". Since a page can now end early at the byte budget, stepping by the requested limit rather than the received count silently skips rows, starting around 5KB average rows. Rewritten to advance byrowCountand stop onnextCursor.tools/table/query_rows.ts(the v1 Table block) droppednextCursorintransformResponse, leaving workflow authors with no correct termination signal. Now surfaced, matchingquery_rows_v2.What was investigated and deliberately NOT changed
Four other suspected regressions were verified and declined on evidence:
useTablemount — real (network deduped, memos and renders not), but a provider refactor of a 1746 + 4616-line pair for unmeasured gain.#6582byte cut /firstBatchCap— both bounds are load-bearing. Reverting the byte cut restores a 400MB worst-case page; every route to a single first batch either re-opens unbounded memory or puts a byte-accounting mismatch on the hottest table read.Type of Change
Testing
9 unit tests: the header is sent in the browser and omitted on the server, an over-long id is dropped rather than broadcast, absent reads as unattributed, and the attributed call-site allowlist is pinned.
Not browser-verified. The check that would confirm it end-to-end: edit a cell on a scrolled table with devtools Network open — today that triggers one
GET /rowsper loaded page ~250–750ms later; it should now trigger none in the acting tab, while a second tab still refetches.Checklist