enhance(core): Check set() Union rows per member - #4179
Conversation
A Union row is now checked against the member its discriminator selects, without the index signature that let a row fall through to another member. Spread updaters on a 30-member Union check about 3x faster than before (TS 6: 9.4s to 3.3s per 1000 calls), and rows that mix members' fields or carry unknown keys are now errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
|
| 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.
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
|
Staff engineer (Cursor agent): LGTM on direction at f919dcc, FOLLOW_UP only, nothing here needs to change before merge. This is a net simplification: Open question: I'd keep it strict (this PR) rather than restore the FOLLOW_UP (docs nuance, take it or leave it): per-member checking only kicks in when the discriminator field has a literal type on each member, like the fixtures' |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
|
Folded the docs nuance into this PR in cdf135d. I confirmed it first: with Generated by Claude Code |
|
Size Change: 0 B Total Size: 103 kB ℹ️ View Unchanged
|
There was a problem hiding this comment.
Benchmark React
Details
| Benchmark suite | Current: 01577f0 | Previous: eb8aacd | Ratio |
|---|---|---|---|
data-client: getlist-100 |
143.89 ops/s (± 5.3%) |
132.46 ops/s (± 4.8%) |
0.92 |
data-client: getlist-500 |
44.05 ops/s (± 4.9%) |
43.29 ops/s (± 5.0%) |
0.98 |
data-client: update-entity |
384.62 ops/s (± 11.0%) |
322.58 ops/s (± 7.5%) |
0.84 |
data-client: update-user |
408.33 ops/s (± 11.5%) |
333.33 ops/s (± 8.6%) |
0.82 |
data-client: getlist-500-sorted |
41.15 ops/s (± 9.3%) |
44.64 ops/s (± 9.3%) |
1.08 |
data-client: update-entity-sorted |
370.37 ops/s (± 7.4%) |
303.03 ops/s (± 6.6%) |
0.82 |
data-client: update-entity-multi-view |
370.37 ops/s (± 7.5%) |
357.14 ops/s (± 9.9%) |
0.96 |
data-client: list-detail-switch-10 |
10.31 ops/s (± 9.0%) |
9.23 ops/s (± 8.8%) |
0.90 |
data-client: update-user-10000 |
80.01 ops/s (± 14.7%) |
77.52 ops/s (± 14.7%) |
0.97 |
data-client: invalidate-and-resolve |
38.31 ops/s (± 6.1%) |
34.42 ops/s (± 3.7%) |
0.90 |
data-client: unshift-item |
224.75 ops/s (± 9.2%) |
194.19 ops/s (± 7.9%) |
0.86 |
data-client: delete-item |
294.12 ops/s (± 6.7%) |
243.9 ops/s (± 2.5%) |
0.83 |
data-client: move-item |
183.5 ops/s (± 10.1%) |
166.67 ops/s (± 9.3%) |
0.91 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: 01577f0 | Previous: eb8aacd | Ratio |
|---|---|---|---|
normalizeLong |
418 ops/sec (±3.90%) |
410 ops/sec (±3.15%) |
0.98 |
normalizeLong Values |
385 ops/sec (±1.24%) |
393 ops/sec (±0.48%) |
1.02 |
normalizeLong Scalar |
349 ops/sec (±3.21%) |
353 ops/sec (±3.42%) |
1.01 |
normalizeLong Scalar update |
915 ops/sec (±0.17%) |
896 ops/sec (±0.90%) |
0.98 |
denormalizeLong |
242 ops/sec (±6.44%) |
226 ops/sec (±6.16%) |
0.93 |
denormalizeLong Values |
224 ops/sec (±5.22%) |
214 ops/sec (±5.18%) |
0.96 |
denormalizeLong donotcache |
1005 ops/sec (±0.97%) |
984 ops/sec (±0.59%) |
0.98 |
denormalizeLong Values donotcache |
746 ops/sec (±0.28%) |
731 ops/sec (±0.26%) |
0.98 |
denormalizeLong Scalar donotcache |
1052 ops/sec (±0.22%) |
1005 ops/sec (±0.42%) |
0.96 |
denormalizeShort donotcache 500x |
1400 ops/sec (±0.15%) |
1397 ops/sec (±0.14%) |
1.00 |
denormalizeShort 500x |
606 ops/sec (±6.14%) |
629 ops/sec (±6.39%) |
1.04 |
denormalizeShort 500x withCache |
6837 ops/sec (±0.13%) |
6684 ops/sec (±0.32%) |
0.98 |
queryShort 500x withCache |
3173 ops/sec (±0.08%) |
3083 ops/sec (±0.59%) |
0.97 |
buildQueryKey All |
58819 ops/sec (±1.48%) |
57185 ops/sec (±0.91%) |
0.97 |
query All withCache |
6342 ops/sec (±2.69%) |
5928 ops/sec (±2.01%) |
0.93 |
denormalizeLong with mixin Entity |
220 ops/sec (±7.81%) |
203 ops/sec (±6.10%) |
0.92 |
denormalizeLong withCache |
7754 ops/sec (±0.18%) |
6654 ops/sec (±0.29%) |
0.86 |
denormalizeLong withCache (Scalar churn) |
7682 ops/sec (±1.22%) |
6500 ops/sec (±0.74%) |
0.85 |
denormalizeLong Values withCache |
5251 ops/sec (±1.74%) |
4766 ops/sec (±1.90%) |
0.91 |
denormalizeLong Scalar withCache |
7953 ops/sec (±0.40%) |
7050 ops/sec (±0.26%) |
0.89 |
denormalizeLong Scalar update withCache |
4098 ops/sec (±1.07%) |
4028 ops/sec (±0.23%) |
0.98 |
denormalizeLong All withCache |
6604 ops/sec (±0.20%) |
6216 ops/sec (±0.39%) |
0.94 |
denormalizeLong Query-sorted withCache |
6635 ops/sec (±1.98%) |
6060 ops/sec (±1.43%) |
0.91 |
denormalizeLongAndShort withEntityCacheOnly |
1834 ops/sec (±0.23%) |
1684 ops/sec (±0.31%) |
0.92 |
denormalize bidirectional 50 |
4708 ops/sec (±8.90%) |
4350 ops/sec (±11.67%) |
0.92 |
denormalize bidirectional 50 donotcache |
41693 ops/sec (±0.27%) |
43258 ops/sec (±0.58%) |
1.04 |
getResponse |
4462 ops/sec (±3.96%) |
4393 ops/sec (±3.68%) |
0.98 |
getResponse (null) |
10265517 ops/sec (±0.56%) |
9494914 ops/sec (±1.41%) |
0.92 |
getResponse (clear cache) |
208 ops/sec (±7.98%) |
190 ops/sec (±9.48%) |
0.91 |
getSmallResponse |
3544 ops/sec (±0.45%) |
3361 ops/sec (±1.09%) |
0.95 |
getSmallInferredResponse |
2894 ops/sec (±0.23%) |
2841 ops/sec (±0.53%) |
0.98 |
getResponse Collection |
4226 ops/sec (±4.75%) |
4463 ops/sec (±3.49%) |
1.06 |
get Collection |
2695 ops/sec (±0.20%) |
2791 ops/sec (±0.37%) |
1.04 |
get Query-sorted |
5123 ops/sec (±1.57%) |
4921 ops/sec (±1.86%) |
0.96 |
setLong |
430 ops/sec (±0.36%) |
435 ops/sec (±0.26%) |
1.01 |
setLongWithMerge |
253 ops/sec (±0.15%) |
238 ops/sec (±1.29%) |
0.94 |
setLongWithSimpleMerge |
267 ops/sec (±0.57%) |
264 ops/sec (±0.78%) |
0.99 |
setSmallResponse 500x |
911 ops/sec (±1.65%) |
895 ops/sec (±1.64%) |
0.98 |
setMany 50x one-per-row |
146 ops/sec (±0.47%) |
141 ops/sec (±0.49%) |
0.97 |
setMany 50 batch |
3563 ops/sec (±1.02%) |
3415 ops/sec (±2.44%) |
0.96 |
setMany 500x one-per-row |
14.93 ops/sec (±0.66%) |
14.24 ops/sec (±0.70%) |
0.95 |
setMany 500 batch |
1400 ops/sec (±0.62%) |
1332 ops/sec (±3.14%) |
0.95 |
This comment was automatically generated by workflow using github-action-benchmark.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4179 +/- ##
=======================================
Coverage 98.08% 98.08%
=======================================
Files 165 165
Lines 3139 3139
Branches 625 625
=======================================
Hits 3079 3079
Misses 18 18
Partials 42 42 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
…ion rows #4179 changed set() row types after #4189 started checking that the playground editor types are current, so the editor-types check is red on master. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H
#4179 changed set() row types after #4189 added the editor-types check, so master's committed core.d.ts was stale. Claude-Session: https://claude.ai/code/session_01N2JUxvTqcHCdBX8Xi1nQq6 Co-authored-by: Claude <noreply@anthropic.com>
#4181) * docs: Generate DevTools MCP and Vue testing skill references from docs The devtools-debugging skill reference (copied byte for byte into the React and Vue skills) and vue-test-utilities (a copy of packages/vue/src/test/README.md) were hand-written, so the build:skills drift check never covered them. - Move the DevTools MCP recipe to docs/core/guides/devtools-debugging.md (framework-aware) and generate both skills' copies from it - Add Vue "Unit testing components" (mountDataClient) and "Unit testing composables" (renderDataCompose) guides as .vue.md overrides of the React pages, and generate the vue-testing skill's references from them - Fix renderDataCompose examples in the vue-testing skill: it is async and `result` is the composable's return value (there is no `result.current`) - Point the test README and JSDoc @see links at the new guides Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H * docs: Simplify Vue testing skill and guides; link Vue testing guides Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H * docs: Link DevTools MCP debugging from Agent Skills Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H * internal(website): Regenerate playground core types for per-member Union rows #4179 changed set() row types after #4189 started checking that the playground editor types are current, so the editor-types check is red on master. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H * docs: Simplify agent debugging links and Vue testing skill Move the DevTools MCP page into getting-started (it is listed there), link it from the framework skill bullets instead of a second "Debugging with agents" section, link DataClientPlugin to its API page, and drop examples the Vue testing skill repeated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H * docs(vue): Serve composables testing guide at /vue/guides/unit-testing-composables The Vue page keeps its unit-testing-hooks.vue.md filename so it shares the React page's doc id (sidebar and framework switcher), and sets a slug so its URL says composables. routeOf now honors absolute slugs so generated skill references link to the same URL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EcBpA1YWwvXC94QHnzQG1H --------- Co-authored-by: Claude <noreply@anthropic.com>
Requested by Nathaniel · project thread
Follow-up to #4133 (unreleased typed
set()values).Motivation
#4133 merged a Union's members into one row type to speed up type checking. That made
prev => ({ ...prev, field })updaters on large Unions about 3x slower than v0.18, and it let rows mix fields from different members. The per-member rows it replaced weren't stricter: their{ [k: string]: unknown }catch-all let a row fall through to another member's row.Solution
Each Union member gets its own row, with no index signature. When each member declares its discriminator as a literal (
readonly type = 'post'), TypeScript picks the member by that discriminator and checks the row against it. A Union row now takes only declared fields, like an Entity row. A key that aschemaAttributefunction reads must therefore be declared on each member (decided: strict rows over keeping the catch-all).TS 6 check time, 1000 calls on a 30-member Union (master, then this PR):
prev => ({ ...prev, f })prev => ({ id: prev.id, f })() => ({ f })Zero loss: with
@ts-expect-errorneutralized, every error master reports still appears on TS 4.0, 4.5, 5.0, 5.4, 6 and 7. The new errors are the mixed-member and unknown-field rows. The v0.19 blog perf rows are re-measured against v0.18.1. The type-check budget is updated:unioninstantiations go up 19% from the per-member rows, but its check time doesn't change.setValuesdrops 71% andsetUpdaters49%.🤖 Generated with Claude Code
https://claude.ai/code/session_01GzkTc2gisCbeWKi5F3v472
Note
Medium Risk
Compile-time-only change that may surface new TypeScript errors on Union
set()calls; runtime store behavior is unchanged.Overview
Tightens
Controller.set()typing for Union schemas by makingSetRowdistributive over union members instead of merging all members into one permissive row. When each member uses a literal discriminator (e.g.readonly type = 'post'), TypeScript checks the value against the selected member only—mixing another member’s fields or unknown keys becomes a compile error, and keys used byschemaAttributemust be declared on each member.Docs (Controller references, changeset, v0.19 blog) are updated to describe this behavior and refreshed perf numbers:
set()plain values and updater functions on large unions are faster than the previous merged-row approach, with updatedscripts/typeperf/budget.jsoninstantiations. React hook tests add@ts-expect-errorcoverage for the new union rules.Reviewed by Cursor Bugbot for commit 01577f0. Bugbot is set up for automated code reviews on this repo. Configure here.