Skip to content

fix(core): Type batch controller.set() rows; allow Union and Invalidate - #4130

Merged
ntucker merged 6 commits into
masterfrom
claude/project-thread-fjrb2j
Oct 4, 2026
Merged

ntucker merged 6 commits into
masterfrom
claude/project-thread-fjrb2j

Conversation

@ntucker

@ntucker ntucker commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Nathaniel · project thread

Fixes # .

Motivation

Follow-up to #4103. The Array overload of controller.set() accepted any Schema[] with untyped rows, so these all typechecked:

ctrl.set([User, Admin], rows); // dev: throws in setReducer; prod: silently uses User
ctrl.set([[User]], rows); // writes nothing
ctrl.set([() => 1], rows);
ctrl.set([User], [1, 'str']); // rows untyped

Solution

Each element is typed by what makes it writable: one row normalizes to one stored entity. That admits Entity, Union (mixed types) and Invalidate (batch delete), as [X], schema.Array(X) or schema.Values(X) (rows keyed by id). Query and All normalize to lists, and Collection is keyed by args that batch set() lacks, so they're excluded. Rows are typed by the Entity's fields.

Changeset (amended rather than added, since #4103's is unreleased):

// Before: TypeScript error on [Ticker], so batches became one set() per row
for (const row of rows) {
  ctrl.set(Ticker, { product_id: row.product_id }, row);
}

// After: one store update
ctrl.set([Ticker], rows);

// Mixed Entity types, batch deletes, and rows keyed by id
const Message = new schema.Union({ ticker: Ticker, trade: Trade }, 'type');
ctrl.set([Message], messages);
ctrl.set([new schema.Invalidate(Ticker)], [{ product_id: 'BTC-USD' }]);
ctrl.set(new schema.Values(Ticker), { 'BTC-USD': row });

Key decisions:

  • "Writable" is structural: Entity (matched by its pk/createIfValid members, since its normalize() is untyped) or a schema whose _normalizeNullable() returns an entity reference (string | { id, schema }), excluding Collection.
  • Rows coerce number | string like EntityFields; object, array and Date fields are unknown because rows are raw pre-normalize input.
  • Polymorphic rows allow extra keys, so a discriminator read by a schemaAttribute function needn't be an Entity field.
  • Entities typed any (e.g. EntityMixin) accept any row.
  • No as key remapping, so the d.ts parses under TS 4.0.

Docs: Controller.md#set-array (Union, Invalidate, Values examples), Values.md, Invalidate.md, and the draft v0.19 blog.

Open questions

Union rows are checked against the union of member fields, so a field valid only on another member isn't caught.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2


Note

Medium Risk
Public Controller typing in @data-client/core may break builds that relied on the old permissive overload, though runtime store behavior is unchanged from the prior batch-set work.

Overview
Tightens the batch controller.set() overload so [Entity], schema.Array(...), and schema.Values(...) accept only schemas that write one stored entity per row, with rows typed from the Entity’s fields (including number/string coercion).

Adds setManyTypes.ts (SetManySchema / SetManyValue) and wires it in Controller.ts, replacing the previous loose Schema[] + readonly {}[] signature. Invalid batch shapes now fail typecheck—e.g. [User, Admin] without a Union, nested arrays, Query/Collection, or array vs object mismatches for Values. Union, Invalidate (batch delete), and Values (id-keyed objects) are explicitly supported.

Docs (Controller #set-array, Invalidate, Values), the changeset, and the v0.19 blog are updated with examples; React useController tests add compile-time @ts-expect-error coverage and a runtime batch test for polymorphic Union, Values, and Invalidate.

Reviewed by Cursor Bugbot for commit 6ba389c. Bugbot is set up for automated code reviews on this repo. Configure here.

…rrays

The Array overload of controller.set() accepted any Schema[] with untyped
rows, so [User, Admin] (throws in dev, uses User in prod), [[User]],
[() => 1] and [{ bogus: 1 }] typechecked, as did ctrl.set([User], [1]).

Now the schema must hold a single Entity or Union, in an array, a
schema.Array, or a schema.Values (which takes rows keyed by id). Rows are
typed by the Entity's fields, with number/string coercion; object values
stay open since rows are raw input. Polymorphic rows may carry a
discriminator that is not an Entity field, and untyped Entities like
EntityMixin accept any row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2
@changeset-bot

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6ba389c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 7 packages
Name Type
@data-client/core Patch
@data-client/react Patch
@data-client/vue Patch
example-benchmark Patch
example-benchmark-react Patch
test-bundlesize Patch
coinbase-lite Patch

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs-site Ignored Ignored Preview Oct 4, 2026 3:09pm UTC

Request Review

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 103 kB

ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 1.46 kB
examples/test-bundlesize/dist/polyfill.js 307 B
examples/test-bundlesize/dist/rdcClient.js 10.9 kB
examples/test-bundlesize/dist/rdcEndpoint.js 8.07 kB
examples/test-bundlesize/dist/rdcNextjs.js 12.3 kB
examples/test-bundlesize/dist/rdcPipeableStream.js 9.63 kB
examples/test-bundlesize/dist/react.js 59.7 kB
examples/test-bundlesize/dist/webpack-runtime.js 784 B

compressed-size-action

@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.90%. Comparing base (de55259) to head (6ba389c).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4130      +/-   ##
==========================================
+ Coverage   97.87%   97.90%   +0.03%     
==========================================
  Files         156      156              
  Lines        3057     3057              
  Branches      612      612              
==========================================
+ Hits         2992     2993       +1     
  Misses         18       18              
+ Partials       47       46       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark React

Details
Benchmark suite Current: 6ba389c Previous: f343f9d Ratio
data-client: getlist-100 129.87 ops/s (± 3.9%) 163.93 ops/s (± 3.6%) 1.26
data-client: getlist-500 40.57 ops/s (± 5.9%) 54.35 ops/s (± 3.3%) 1.34
data-client: update-entity 317.54 ops/s (± 5.9%) 370.37 ops/s (± 6.8%) 1.17
data-client: update-user 307.77 ops/s (± 5.2%) 370.88 ops/s (± 7.1%) 1.21
data-client: getlist-500-sorted 41.58 ops/s (± 10.1%) 54.06 ops/s (± 9.7%) 1.30
data-client: update-entity-sorted 285.71 ops/s (± 5.5%) 344.83 ops/s (± 4.9%) 1.21
data-client: update-entity-multi-view 303.03 ops/s (± 7.6%) 377.49 ops/s (± 6.9%) 1.25
data-client: list-detail-switch-10 7.5 ops/s (± 7.0%) 12.84 ops/s (± 8.0%) 1.71
data-client: update-user-10000 72.47 ops/s (± 13.2%) 80.01 ops/s (± 14.1%) 1.10
data-client: invalidate-and-resolve 34.6 ops/s (± 4.4%) 44.44 ops/s (± 5.3%) 1.28
data-client: unshift-item 192.31 ops/s (± 5.1%) 238.1 ops/s (± 4.1%) 1.24
data-client: delete-item 263.16 ops/s (± 3.8%) 333.33 ops/s (± 4.4%) 1.27
data-client: move-item 161.29 ops/s (± 8.4%) 194.19 ops/s (± 8.7%) 1.20

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: 6ba389c Previous: f343f9d Ratio
normalizeLong 435 ops/sec (±4.13%) 434 ops/sec (±3.14%) 1.00
normalizeLong Values 388 ops/sec (±0.36%) 391 ops/sec (±0.35%) 1.01
normalizeLong Scalar 356 ops/sec (±2.59%) 368 ops/sec (±2.85%) 1.03
normalizeLong Scalar update 901 ops/sec (±0.15%) 927 ops/sec (±0.20%) 1.03
denormalizeLong 239 ops/sec (±6.54%) 236 ops/sec (±6.04%) 0.99
denormalizeLong Values 217 ops/sec (±4.54%) 221 ops/sec (±4.87%) 1.02
denormalizeLong donotcache 1071 ops/sec (±0.74%) 1019 ops/sec (±0.44%) 0.95
denormalizeLong Values donotcache 765 ops/sec (±0.22%) 750 ops/sec (±0.20%) 0.98
denormalizeLong Scalar donotcache 1180 ops/sec (±0.25%) 1006 ops/sec (±0.45%) 0.85
denormalizeShort donotcache 500x 1397 ops/sec (±0.09%) 1405 ops/sec (±0.14%) 1.01
denormalizeShort 500x 570 ops/sec (±5.72%) 620 ops/sec (±7.08%) 1.09
denormalizeShort 500x withCache 7208 ops/sec (±0.13%) 6738 ops/sec (±0.10%) 0.93
queryShort 500x withCache 3322 ops/sec (±0.69%) 3159 ops/sec (±1.18%) 0.95
buildQueryKey All 51476 ops/sec (±0.61%) 58235 ops/sec (±0.52%) 1.13
query All withCache 6668 ops/sec (±2.45%) 5985 ops/sec (±2.74%) 0.90
denormalizeLong with mixin Entity 197 ops/sec (±7.63%) 210 ops/sec (±6.92%) 1.07
denormalizeLong withCache 7027 ops/sec (±0.32%) 6951 ops/sec (±0.21%) 0.99
denormalizeLong withCache (Scalar churn) 6951 ops/sec (±1.20%) 6895 ops/sec (±1.03%) 0.99
denormalizeLong Values withCache 6500 ops/sec (±2.06%) 5051 ops/sec (±1.79%) 0.78
denormalizeLong Scalar withCache 7383 ops/sec (±0.50%) 7384 ops/sec (±0.39%) 1.00
denormalizeLong Scalar update withCache 5576 ops/sec (±0.73%) 4038 ops/sec (±0.53%) 0.72
denormalizeLong All withCache 6838 ops/sec (±0.34%) 6119 ops/sec (±0.24%) 0.89
denormalizeLong Query-sorted withCache 7010 ops/sec (±2.49%) 6277 ops/sec (±1.34%) 0.90
denormalizeLongAndShort withEntityCacheOnly 1655 ops/sec (±0.19%) 1796 ops/sec (±0.29%) 1.09
denormalize bidirectional 50 4252 ops/sec (±11.16%) 4680 ops/sec (±9.78%) 1.10
denormalize bidirectional 50 donotcache 44889 ops/sec (±0.17%) 41122 ops/sec (±1.08%) 0.92
getResponse 5145 ops/sec (±5.22%) 4571 ops/sec (±2.19%) 0.89
getResponse (null) 10047596 ops/sec (±0.48%) 10005799 ops/sec (±1.66%) 1.00
getResponse (clear cache) 195 ops/sec (±8.59%) 199 ops/sec (±9.42%) 1.02
getSmallResponse 3732 ops/sec (±1.51%) 3592 ops/sec (±0.25%) 0.96
getSmallInferredResponse 3089 ops/sec (±0.15%) 2712 ops/sec (±1.08%) 0.88
getResponse Collection 5351 ops/sec (±3.23%) 4437 ops/sec (±3.60%) 0.83
get Collection 3676 ops/sec (±0.37%) 2822 ops/sec (±0.21%) 0.77
get Query-sorted 5816 ops/sec (±2.85%) 4979 ops/sec (±1.73%) 0.86
setLong 426 ops/sec (±0.39%) 433 ops/sec (±0.29%) 1.02
setLongWithMerge 251 ops/sec (±0.68%) 252 ops/sec (±0.96%) 1.00
setLongWithSimpleMerge 266 ops/sec (±0.85%) 272 ops/sec (±0.18%) 1.02
setSmallResponse 500x 884 ops/sec (±2.28%) 918 ops/sec (±1.40%) 1.04
setMany 50x one-per-row 136 ops/sec (±0.55%) 151 ops/sec (±0.37%) 1.11
setMany 50 batch 3636 ops/sec (±2.72%) 3540 ops/sec (±2.68%) 0.97
setMany 500x one-per-row 13.91 ops/sec (±0.46%) 15.4 ops/sec (±0.69%) 1.11
setMany 500 batch 1440 ops/sec (±3.38%) 1393 ops/sec (±3.20%) 0.97

This comment was automatically generated by workflow using github-action-benchmark.

Name the Entity map shape once, drop constraint members the value type
already enforces, reuse Denormalize<> for Array/Values rows, and collapse
SetField's tail branches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/controller/Controller.ts Outdated
Restore the schemaKey() member; schema.Object's queryKey() returns any, so
queryKey(): undefined alone no longer excluded it. Adds type tests for
schema.Object with keyed rows and schema.Lazy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b86e50c. Configure here.

Comment thread packages/react/src/hooks/__tests__/useController/set.tsx Outdated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2
Type batch set() elements by what makes them writable: one row normalizes to
one stored entity (_normalizeNullable() returns an entity ref). That admits
Entity, Union and Invalidate, and keeps out Query/All (lists) and
Collection (keyed by args). Docs lead with Union and Invalidate examples.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2
@ntucker ntucker changed the title fix(core): Type batch controller.set() rows and reject multi-Entity arrays fix(core): Type batch controller.set() rows; allow Union and Invalidate Oct 4, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2
@ntucker
ntucker merged commit 8b3bb89 into master Oct 4, 2026
27 checks passed
@ntucker
ntucker deleted the claude/project-thread-fjrb2j branch October 4, 2026 15:12
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.

2 participants