fix(core): Type batch controller.set() rows; allow Union and Invalidate - #4130
Conversation
…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
|
| 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Size Change: 0 B Total Size: 103 kB ℹ️ View Unchanged
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbthieGWrKshPjDkH7mGz2

Requested by Nathaniel · project thread
Fixes # .
Motivation
Follow-up to #4103. The Array overload of
controller.set()accepted anySchema[]with untyped rows, so these all typechecked: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)orschema.Values(X)(rows keyed by id). Query and All normalize to lists, and Collection is keyed by args that batchset()lacks, so they're excluded. Rows are typed by the Entity's fields.Changeset (amended rather than added, since #4103's is unreleased):
Key decisions:
pk/createIfValidmembers, since itsnormalize()is untyped) or a schema whose_normalizeNullable()returns an entity reference (string | { id, schema }), excluding Collection.number | stringlikeEntityFields; object, array and Date fields areunknownbecause rows are raw pre-normalize input.schemaAttributefunction needn't be an Entity field.any(e.g.EntityMixin) accept any row.askey 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/coremay 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(...), andschema.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 inController.ts, replacing the previous looseSchema[]+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; ReactuseControllertests add compile-time@ts-expect-errorcoverage 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.