Skip to content

feat(react): public rejected-writes API (rejectedWrites + onRejectedWriteChange) - #564

Merged
blove merged 14 commits into
mainfrom
blove/wonderful-dijkstra-ffcdd1
Sep 2, 2026
Merged

feat(react): public rejected-writes API (rejectedWrites + onRejectedWriteChange)#564
blove merged 14 commits into
mainfrom
blove/wonderful-dijkstra-ffcdd1

Conversation

@blove

@blove blove commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Since #550/#553/#557/#559, an invalid rows/derivations/query update is a rejected write: the grid keeps its last-good value and stays alive. The only signal was a console warning that latches per fault kind — and there was no API to ask whether the rendered rows match the ones you passed. Now there is:

  • model.rejectedWrites: PretableRejectedWrites on usePretable's return — a per-kind record { rows, derivations, query }, each null when in sync or { kind, code, message, columnId? } describing the most recent rejection. Nothing latches; every rejection replaces the record; a slot clears on its own when a valid value lands (published pre-paint, so no painted frame is ever stale).
  • onRejectedWriteChange on PretableSurface and LabeledGridSurface — fires on every transition including recovery; silent at mount, on unrelated renders, and on callback-prop swaps (transition events, not state sync).
  • useLocalRowModel rejections (rows and derivations) surface through the same record via a react-internal Symbol channel on the model instance — model-mode consumers get the same answer, no false "in sync".

Console warnings are unchanged. Fatal faults (disposed-model, reentrant-mutation, foreign errors) still throw.

How

Generalizes #561's notifying RowsWriteState store to per-kind {refused, fault} slots with identity-preserving publish normalization; the public record is a fault-only-deps memo returning a shared frozen empty identity, so record identity moves exactly on fault transitions (pinned by paging/no-change tests). The async query path publishes read-modify-write from applyQuery (generation-gated against stale rejections).

Design spec: docs/superpowers/specs/2026-09-01-rejected-writes-public-api-design.md (includes two as-built notes: the local channel carries derivations too, and invalid initial rows still throw at model creation — a creation fault, not a write).

Verification

  • packages/react: 128 files / 1778 tests green; 18 new tests in rejected-writes-public-api.test.tsx, every load-bearing assertion mutation-verified (memo deps, both query clears, channel merge, slot carry-forward, ref-update, fresh-literal empty)
  • pnpm build && pnpm api:check green; react.api.md diff is exactly the four new symbols
  • website docs guard: 104 files / 613 tests green; new "Rejected writes" docs section on grid/pretable-surface.mdx
  • Changeset: @pretable/react minor

🤖 Generated with Claude Code

blove and others added 13 commits September 1, 2026 15:46
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generalizes usePretable's notifying rows-write store to carry a fault
record per write kind (rows, derivations, query), wires all three
rejection guards to publish through it, and derives a fault-only-keyed
PretableRejectedWrites record exposed as model.rejectedWrites. The
shared EMPTY_REJECTED_WRITES identity is returned whenever every slot
is null, and slot normalization keeps record identity stable across
no-op republishes and ordinary windowed paging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The corrected-query recovery test could not see the main publish's
controlledQueryChanged clear branch: its new query lands synchronously
in the same effect, so the landed-transition publish masks it. Pairing
the corrected query with a simultaneous derivations change defers
applyQuery behind the derivations transition, leaving a window where
only the main publish can have cleared the slot — asserted before any
microtask runs, and again after the deferred re-apply lands. Verified
by mutation: always-previous on that branch fails exactly this test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two pins the review asked for: one commit rejecting rows AND
derivations must surface both faults from the single consolidated
publish (the reason the publish sits below the derivations block), and
a query rejected on the deferred .then path must publish its fault via
the read-modify-write over the settled snapshot, after the interim
main-publish clear.

Comment-only fixes alongside: RejectedWriteSlot.refused documented as
replacement semantics (it never drives a clear), the slotOrPrevious
rows-vs-slots normalization asymmetry stated as intended, and the
plan-artifact localSlots sentence deleted from the memo comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A consumer doing useLocalRowModel({rows}) + <PretableSurface model={m}>
never runs usePretable's own guards, so the public rejectedWrites record
answered "in sync" for a grid that had diverged. A Symbol-keyed
notifying store attached to the model instance now carries the local
hook's rows/derivations faults into usePretable's merged record; query
stays null by construction for this entry point.

The merge is a disjoint union via ??, not precedence: in model mode the
own slots are always null (the effect returns early), in rows mode the
channel slots are always null (the owned model carries no channel).

Pinned by mutation: replacing readLocalRejectedWrites(rowModel) with
undefined fails both model-mode tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two coverage pins on the useLocalRowModel rejection channel: a commit
that recovers only rows must carry the standing derivations fault
forward (the previousSlots seeding — verified by mutation: seeding the
slots to null fails the test), and a fault must follow its model
instance across a model-prop swap rather than leak to a clean model.
Plus one doc line: attachLocalRejectedWrites is at-most-once per model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PretableSurface (and LabeledGridSurface, byte-parallel to
onTelemetryChange) now accepts onRejectedWriteChange, notified whenever
the PretableRejectedWrites record changes identity: every rejection of
any kind, and every recovery — nothing latches, unlike the console
warning. A layout effect compares the record against the last DELIVERED
one, seeded with the mount value, so the callback stays silent at mount,
on valid updates, on paging, and when only the callback prop changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A derivations fault must drive onRejectedWriteChange too — the callback
is not rows-only — and a callback swapped in with no data change gets no
catch-up call for the standing record, only the transitions after it
arrives. The prop's JSDoc now states the mechanism (seeded compare,
transition-only) rather than the mount-time coincidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment contrasted the channel's effect-publish clear with "usePretable's
own rows slot" — true only of the internal rejectedRows identity; the public
rowsFault clears via the same effect publish as the channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove enabled auto-merge (squash) September 2, 2026 00:27
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
pretable Ignored Ignored Sep 2, 2026 12:28am UTC

Request Review

GHSA-73wf-gq98-2v4g

Two new HIGH advisories on browserslist <=4.28.6 (transitive via
eslint-plugin-react-hooks -> @babel/core) fail the required security-audit
check on every branch. Lockfile-only in-range bump; pnpm audit is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Vercel preview ready

Preview: https://pretable-d8l4lj5xd-cacheplane.vercel.app
Commit: 4f200117059a1bedd9e5f5d124052fff48023009

Updated automatically by the deploy-preview job.

@blove
blove merged commit 5f39cfe into main Sep 2, 2026
21 checks passed
@blove
blove deleted the blove/wonderful-dijkstra-ffcdd1 branch September 2, 2026 00:46
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