Skip to content

fix(normalizr): Track INVALID and null nested entities as memo dependencies - #4460

Open
ntucker wants to merge 12 commits into
masterfrom
fix/memo-dependency-gaps
Open

ntucker wants to merge 12 commits into
masterfrom
fix/memo-dependency-gaps

Conversation

@ntucker

@ntucker ntucker commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Nathaniel

Motivation

Bug fix. A component reads a parent entity, and a nested entity of that parent is INVALID (it failed validation) or null in the store.

Example: an Article has author: '2', and User '2' is INVALID. Then a valid User '2' arrives. Expected: the article shows the new author. Actual: the cached article keeps the old, empty author.

Solution

  • Behavior: a parent recomputes when a nested INVALID or null entity changes, as it already did for a missing one. unvisitEntity records these entities as dependencies. Blank ids are still not dependencies, because they cannot be looked up.
  • Internal: WeakMap keys must be objects. depKey() gives each non-object value a stable object stand-in.

Assumptions and validation

  • High: the INVALID and null tests in MemoCache.ts fail on master and pass here. The unit suite passes.
  • High: the memo invariants, now written in packages/normalizr/AGENTS.md, hold. fix(normalizr): Keep memo WeakMaps one level deep #4459 builds on them.
  • Medium: I ran coverage on the memo and denormalize files only. Each new branch is covered.
  • High: speed, measured. Six alternating master/fix(normalizr): Track INVALID and null nested entities as memo dependencies #4460 rounds of examples/benchmark and three of benchmark-react (data-client only) on one quiet machine (Ryzen 9 7950X, WSL2, Node 26). Between runs, a case moves by up to about ±5%, so only larger changes are real. Full tables are under "Benchmarks" below.
    • Cached reads of lists with nested null or INVALID children cost 16–21%, with 25% of children affected. They are now dependencies, so each cached read checks them, as it already checks missing children on master. This cost is the fix.
    • Cold reads of those lists cost 6–8%. A child that is not an object now records only its dependency: no closure and no entity-cache entry, because there is no value worth caching. This also makes the 25%-missing cold read 10% faster than master. Measured in three more alternating rounds (master, the previous head, this head); see "Non-object children" under "Benchmarks".
    • Other cached reads are 0–6% slower (worst: denormalizeLong All withCache -6.4%, Query-sorted -4.5%, queryShort 500x -4.4%). That is the depKey() call on each dependency check. A smaller depKey for V8 to inline measured no better, so I kept this one.
    • Cold reads, normalize and writes are within ±2%.
    • benchmark-react: all scenarios are within their run-to-run spread (5–17%). update-entity-sorted (-10.9%) and delete-item (-7.7%) are the largest changes, but those two vary the most between runs.

GOALS impact

  • Data integrity: positive. Views stay consistent with the store.
  • Speed: slightly negative. Cached reads of lists with nested null or INVALID children are 16–21% slower, because those children are now tracked (the fix); cold reads of them are 6–8% slower, and missing children read 10% faster cold. Other cached reads are 0–6% slower from depKey(). Other cold reads and writes are unchanged.
  • Bundle size: +80 B brotli to @data-client/normalizr (+2.3%), against current master. The non-object path adds 8 B of that.
  • V8: neutral. No deopt loops and no new deopt reasons; the last cleanup measured 617 deopt events against 629 before it, in one run each. No inline-cache site is worse than on master, and the memo functions reach the same tiers. For the non-object path: dependOn optimizes (Maglev) with no deopts, its call site stays monomorphic, and getEntity has fewer deopts than before (no "wrong feedback cell").

Benchmarks

Non-object children, examples/benchmark, ops/s, median of 3 alternating rounds
case master before (de8c335) now now vs master
denormalizeLong nested 25% missing 495 506 546 +10.3%
denormalizeLong nested 25% null 587 519 552 -6.0%
denormalizeLong nested 25% INVALID 602 507 554 -8.0%
denormalizeLong nested 25% missing withCache 11,951 11,466 11,491 -3.8%
denormalizeLong nested 25% null withCache 14,923 11,781 11,889 -20.3%
denormalizeLong nested 25% INVALID withCache 14,731 11,683 11,559 -21.5%
denormalizeLong 496 479 477 -3.8%
denormalizeLong withCache 11,879 11,016 11,415 -3.9%
examples/benchmark, ops/s, median of 6 alternating rounds
case master 4460 change rounds
normalizeLong 767 772 +0.7% 6/6
normalizeLong Values 674 678 +0.5% 6/6
normalizeLong Scalar 664 664 +0.0% 6/6
normalizeLong Scalar update 1,651 1,628 -1.4% 6/6
denormalizeLong 537 527 -1.9% 6/6
denormalizeLong Values 480 476 -0.8% 6/6
denormalizeLong donotcache 1,896 1,914 +0.9% 6/6
denormalizeLong nested 25% missing 560 556 -0.8% 6/6
denormalizeLong nested 25% null 672 558 -16.8% 6/6
denormalizeLong nested 25% INVALID 670 556 -17.0% 6/6
denormalizeLong Values donotcache 1,320 1,323 +0.2% 6/6
denormalizeLong Scalar donotcache 2,067 2,014 -2.6% 6/6
denormalizeShort donotcache 500x 2,439 2,480 +1.7% 6/6
denormalizeShort 500x 1,310 1,312 +0.1% 6/6
denormalizeShort 500x withCache 13,206 13,066 -1.1% 6/6
queryShort 500x withCache 6,308 6,029 -4.4% 6/6
buildQueryKey All 96,040 96,362 +0.3% 6/6
query All withCache 9,882 10,104 +2.3% 6/6
denormalizeLong with mixin Entity 498 501 +0.6% 6/6
denormalizeLong withCache 11,959 11,890 -0.6% 6/6
denormalizeLong withCache (Scalar churn) 11,900 11,872 -0.2% 6/6
denormalizeLong nested 25% missing withCache 12,222 12,328 +0.9% 6/6
denormalizeLong nested 25% null withCache 15,155 12,494 -17.6% 6/6
denormalizeLong nested 25% INVALID withCache 14,868 12,448 -16.3% 6/6
denormalizeLong Values withCache 9,120 8,942 -2.0% 6/6
denormalizeLong Scalar withCache 13,876 13,398 -3.4% 6/6
denormalizeLong Scalar update withCache 7,706 7,454 -3.3% 6/6
denormalizeLong All withCache 11,964 11,194 -6.4% 6/6
denormalizeLong Query-sorted withCache 10,962 10,466 -4.5% 6/6
denormalizeLongAndShort withEntityCacheOnly 2,863 2,888 +0.9% 6/6
denormalize bidirectional 50 9,330 9,183 -1.6% 6/6
denormalize bidirectional 50 donotcache 78,436 78,418 -0.0% 6/6
getResponse 6,966 6,722 -3.5% 6/6
getResponse (null) 18,735,359 18,569,344 -0.9% 6/6
getResponse (clear cache) 464 466 +0.5% 6/6
getSmallResponse 7,400 7,085 -4.3% 6/6
getSmallInferredResponse 5,587 5,482 -1.9% 6/6
getResponse Collection 8,073 8,114 +0.5% 6/6
get Collection 7,020 7,084 +0.9% 6/6
get Query-sorted 8,532 8,207 -3.8% 6/6
setLong 781 788 +0.9% 6/6
setLongWithMerge 465 462 -0.6% 6/6
setLongWithSimpleMerge 492 491 -0.2% 6/6
setSmallResponse 500x 1,662 1,660 -0.2% 6/6
setMany 50x one-per-row 226 222 -1.6% 6/6
setMany 50 batch 6,519 6,428 -1.4% 6/6
setMany 500x one-per-row 23 23 -0.4% 6/6
setMany 500 batch 2,782 2,790 +0.3% 6/6
benchmark-react (data-client), ops/s, median of 3 alternating rounds
case master 4460 change rounds
getlist-100 21 21 +0.2% 3/3
getlist-500 13 13 -0.9% 3/3
update-entity 955 1,000 +4.8% 3/3
update-user 1,000 1,000 +0.0% 3/3
getlist-500-sorted 13 13 +0.8% 3/3
update-entity-sorted 801 714 -10.9% 3/3
update-entity-multi-view 833 801 -3.8% 3/3
list-detail-switch-10 33 35 +6.1% 3/3
update-user-10000 141 137 -2.8% 3/3
invalidate-and-resolve 10 10 +0.2% 3/3
unshift-item 588 572 -2.8% 3/3
delete-item 833 769 -7.7% 3/3
move-item 426 426 +0.0% 3/3

🤖 Generated with Claude Code


Note

Medium Risk
Changes core denormalize memo dependency tracking and cache lookup keys; correctness is heavily tested but cached-read paths see a small perf cost from depKey() on each dependency step.

Overview
Fixes stale parent denormalization when a nested child was INVALID, null, or missing and later becomes a valid entity—cached parents now recompute instead of keeping the old nested shape.

Memo / denormalize: unvisitEntity calls new cache.dependOn() for non-object store values (still skips blank ids). That records them in WeakDependencyMap without the old UNDEF + getEntity shortcut. depKey() maps undefined, null, INVALID, and other primitives to stable object keys for WeakMap chains.

Docs & tests: “Memo invariants” land in packages/normalizr/AGENTS.md (with agent rule pointers). Coverage adds MemoCache nested-dependency cases, WeakDependencyMap / depKey tests, React concurrent-memo.web.tsx, and benchmark variants for 25% nested missing|null|INVALID.

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

…encies

unvisitEntity returned early for a nested entity whose stored value was
INVALID or null, without recording it as a dependency. A cached parent
kept returning its old result after that entity became valid again.
Both now go through cache.getEntity like a missing entity does, keyed by
depKey(), which gives each non-object value a stable object stand-in.

Documents the memo invariants in packages/normalizr/AGENTS.md, with the
two accepted limitations, and adds a scoped agent rule pointing at them.
Adds a ReactDOM test that counts memo misses under concurrent rendering.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 595ead9

The changes in this PR will be included in the next version bump.

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

Not sure what this means? Click here to learn what changesets are.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ntucker

ntucker commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): Staff review at 6d2dbfb1 (Draft). Verdict: LGTM with follow-ups, nothing blocking.

The fix is the right size. Recording INVALID and null children through cache.getEntity, the same way a missing child already goes through UNDEF, closes the gap without a new mechanism, and depKey() is applied on both sides of WeakDependencyMap (set and both get paths), so stored and looked-up keys agree. depKey is idempotent on objects, so the deps GlobalCache records with an already-converted key round-trip fine.

Things to settle before this lands, in order of importance:

  1. Landing order with fix(normalizr): Keep memo WeakMaps one level deep #4459. Both PRs add .cursor/rules/memo-invariants.mdc, .claude/rules/memo-invariants.md, the "Memo invariants" section in packages/normalizr/AGENTS.md, concurrent-memo.web.tsx, and a blog line, and both edit the get/set paths in WeakDependencyMap.ts. The texts already disagree: fix(normalizr): Keep memo WeakMaps one level deep #4459 says "all seven" and lists this bug as a "Known gap" under invariant 2, while this PR says "all of them" and has six. The PR body says fix(normalizr): Keep memo WeakMaps one level deep #4459 builds on this one, but fix(normalizr): Keep memo WeakMaps one level deep #4459 is Ready and this is Draft. Whichever lands second needs to drop its copies of the shared files, re-apply depKey() to the other PR's WeakDependencyMap shape, and remove the "Known gap" sentence (or add invariant 7), so the doc doesn't describe a bug that's already fixed.
  2. Benchmark before Ready. depKey() now runs on every dependency compare in WeakDependencyMap.get, which is the hottest path in denormalize. The object case returns on the first check, so I expect the cost to be noise, but I'd like the Node numbers from the open question here before this goes Ready.
  3. Optional simplification, your call. unvisitEntity now has three near-identical "record this as a dependency" blocks (missing, INVALID, null). They could be one branch: when inputIsId and the entity is null, a symbol, or undefined with a usable id, call cache.getEntity(entityOrId, schema, depKey(entity), ...) and store schema.denormalize(entity, delegate) for the symbol case or entity otherwise. The UNDEF comment block would then read as the explanation for all three. Separately, depKey's module-level Map could be two fixed constants (null and INVALID are the only non-object values that reach it from the store), which drops a lookup and any chance of the map growing if someone stores primitives in an entity table. Neither changes behavior, so it's fine to leave as is.

What I checked: I read the full diff, plus globalCache.ts on master and unvisit.ts at this head, to confirm the new branches feed getEntity the converted key and that paths() and GC only see {key, pk} paths (the same as the existing UNDEF case). I compared the shared files against #4459 at ecea0af6. I didn't run the tests or benchmarks; that the INVALID and null tests fail on master comes from the PR body, not from me.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Docs preview: https://docs-site-pr-4460-data-client.vercel.app

Built from 595ead9 merged into master (this build). Locales: en (the merge queue builds every locale).

unvisitEntity handled missing, null and INVALID nested entities in three
near-identical branches. One branch now records all three, and
WeakDependencyMap maps its root key through depKey(), so callers pass the
raw value. Tests reuse the shared Article and makeGetTodo fixtures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ntucker

ntucker commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM + FOLLOW_UP at 1be7879.

The refactor is a real entropy win: three near-identical unvisitEntity branches become one, and depKey() is now applied only inside WeakDependencyMap (root key in get, every key in set), so callers and the recorded dependencies[].entity carry the raw value and there's one normalization point instead of callers remembering to pass UNDEF/depKey(...). Blank-id behavior is unchanged (a blank id can't resolve to INVALID, so moving the symbol check after the dependency branch loses nothing).

Follow-up (no change needed here): this commit exists only on #4460. #4459 already merged an earlier cut of this branch, so the two now diverge again (this branch is 1 ahead, 10 behind #4459's head). Whichever lands first, the other needs this refactor folded in, or the three-branch version and the caller-side depKey calls come back on master. Small note for later: the merged branch now also records any other non-object entity value (e.g. a stray string) as a dependency through depKey's strong Map. That's harmless for real entity tables, but worth keeping in mind if one ever holds primitives.

Validated: I read the commit diff plus WeakDependencyMap.ts, UNDEF.ts and GlobalCache.getEntity at 1be7879, and code-searched for other UNDEF consumers (none outside those files). I did not run tests. The tightened avoidableMisses(...).toBe(0) assertions rely on CI, and CircleCI was still pending when I checked.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T02:38:04.317255Z 595ead9 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Size Change: +113 B (+0.1%)

Total Size: 112 kB

📦 View Changed
Filename Size Change
examples/test-bundlesize/dist/rdcClient.js 10.9 kB +37 B (+0.34%)
examples/test-bundlesize/dist/rdcNextjs.js 12.4 kB +34 B (+0.27%)
examples/test-bundlesize/dist/rdcPipeableStream.js 9.69 kB +42 B (+0.44%)
ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 1.45 kB
examples/test-bundlesize/dist/polyfill.js 301 B
examples/test-bundlesize/dist/rdcEndpoint.js 8.04 kB
examples/test-bundlesize/dist/react.js 68.6 kB
examples/test-bundlesize/dist/webpack-runtime.js 698 B

compressed-size-action

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1be7879abb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/** `WeakMap` key for an entity value: the object itself, `UNDEF` for
* `undefined`, or a stable stand-in for any other value (`null`, `INVALID`). */
export function depKey(entity: unknown): object {
if (typeof entity === 'object' && entity !== null) return entity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve function references as weak dependency keys

When an Entity class is passed to MemoCache.buildQueryKey(), BaseDelegate.tracked() records that constructor as the first dependency. Functions are valid WeakMap keys, but this guard excludes them, so every queried Entity constructor is now stored strongly in the module-global valueKeys map. Dynamically created schemas and anything their closures retain consequently survive even after their Controller and MemoCache are discarded; a forced-GC check confirmed this retention. Return functions unchanged alongside non-null objects rather than allocating permanent stand-ins for them.

AGENTS.md reference: packages/normalizr/AGENTS.md:L22-L22

Useful? React with 👍 / 👎.

@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: 595ead9 Previous: 8817f47 Ratio
data-client: getlist-100 215.08 ops/s (± 7.6%) 222.22 ops/s (± 8.5%) 1.03
data-client: getlist-500 80.97 ops/s (± 4.4%) 73.27 ops/s (± 6.4%) 0.90
data-client: update-entity 500 ops/s (± 12.1%) 666.67 ops/s (± 6.1%) 1.33
data-client: update-user 526.32 ops/s (± 8.0%) 625 ops/s (± 6.8%) 1.19
data-client: getlist-500-sorted 67.8 ops/s (± 9.6%) 64.78 ops/s (± 9.3%) 0.96
data-client: update-entity-sorted 384.62 ops/s (± 5.6%) 444.66 ops/s (± 10.7%) 1.16
data-client: update-entity-multi-view 454.55 ops/s (± 9.5%) 555.56 ops/s (± 7.0%) 1.22
data-client: list-detail-switch-10 22.78 ops/s (± 9.9%) 20.64 ops/s (± 8.0%) 0.91
data-client: update-user-10000 119.06 ops/s (± 7.5%) 125.79 ops/s (± 11.2%) 1.06
data-client: invalidate-and-resolve 65.36 ops/s (± 5.4%) 60.61 ops/s (± 7.1%) 0.93
data-client: unshift-item 357.14 ops/s (± 4.8%) 384.62 ops/s (± 0.0%) 1.08
data-client: delete-item 555.56 ops/s (± 11.2%) 555.56 ops/s (± 9.1%) 1
data-client: move-item 289.92 ops/s (± 10.3%) 307.77 ops/s (± 6.0%) 1.06

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: 595ead9 Previous: 7adefee Ratio
normalizeLong 694 ops/sec (±6.35%) 693 ops/sec (±3.56%) 1.00
normalizeLong Values 609 ops/sec (±1.13%) 607 ops/sec (±1.11%) 1.00
normalizeLong Scalar 547 ops/sec (±3.02%) 544 ops/sec (±2.39%) 0.99
normalizeLong Scalar update 1587 ops/sec (±1.60%) 1574 ops/sec (±1.86%) 0.99
denormalizeLong 346 ops/sec (±6.09%) 349 ops/sec (±5.61%) 1.01
denormalizeLong Values 314 ops/sec (±4.52%) 322 ops/sec (±3.99%) 1.03
denormalizeLong donotcache 1777 ops/sec (±1.27%) 1820 ops/sec (±2.03%) 1.02
denormalizeLong nested 25% missing 356 ops/sec (±5.24%)
denormalizeLong nested 25% null 354 ops/sec (±5.02%)
denormalizeLong nested 25% INVALID 380 ops/sec (±4.59%)
denormalizeLong Values donotcache 1253 ops/sec (±1.74%) 1151 ops/sec (±5.18%) 0.92
denormalizeLong Scalar donotcache 1725 ops/sec (±0.72%) 1909 ops/sec (±0.48%) 1.11
denormalizeShort donotcache 500x 2481 ops/sec (±1.03%) 2462 ops/sec (±1.91%) 0.99
denormalizeShort 500x 905 ops/sec (±6.47%) 899 ops/sec (±7.30%) 0.99
denormalizeShort 500x withCache 11934 ops/sec (±2.20%) 9181 ops/sec (±2.20%) 0.77
queryShort 500x withCache 4629 ops/sec (±1.15%) 5359 ops/sec (±0.74%) 1.16
buildQueryKey All 88743 ops/sec (±2.19%) 98489 ops/sec (±0.41%) 1.11
query All withCache 9264 ops/sec (±5.22%) 10256 ops/sec (±5.77%) 1.11
denormalizeLong with mixin Entity 276 ops/sec (±5.87%) 335 ops/sec (±6.11%) 1.21
denormalizeLong withCache 11912 ops/sec (±3.33%) 12871 ops/sec (±0.43%) 1.08
denormalizeLong withCache (Scalar churn) 11921 ops/sec (±2.19%) 12670 ops/sec (±1.00%) 1.06
denormalizeLong nested 25% missing withCache 10927 ops/sec (±0.34%)
denormalizeLong nested 25% null withCache 11668 ops/sec (±1.63%)
denormalizeLong nested 25% INVALID withCache 11035 ops/sec (±2.65%)
denormalizeLong Values withCache 9169 ops/sec (±1.87%) 9804 ops/sec (±1.03%) 1.07
denormalizeLong Scalar withCache 12965 ops/sec (±1.15%) 13747 ops/sec (±0.99%) 1.06
denormalizeLong Scalar update withCache 7234 ops/sec (±1.42%) 7523 ops/sec (±0.38%) 1.04
denormalizeLong All withCache 11590 ops/sec (±0.26%) 13111 ops/sec (±0.23%) 1.13
denormalizeLong Query-sorted withCache 9966 ops/sec (±5.64%) 10436 ops/sec (±6.87%) 1.05
denormalizeLongAndShort withEntityCacheOnly 2708 ops/sec (±1.53%) 3035 ops/sec (±1.13%) 1.12
denormalize bidirectional 50 6030 ops/sec (±10.77%) 7229 ops/sec (±9.22%) 1.20
denormalize bidirectional 50 donotcache 66925 ops/sec (±2.59%) 74415 ops/sec (±0.62%) 1.11
getResponse 7383 ops/sec (±4.85%) 6850 ops/sec (±6.61%) 0.93
getResponse (null) 17967514 ops/sec (±1.88%) 19028450 ops/sec (±3.52%) 1.06
getResponse (clear cache) 264 ops/sec (±8.19%) 333 ops/sec (±7.84%) 1.26
getSmallResponse 5048 ops/sec (±2.21%) 6392 ops/sec (±0.50%) 1.27
getSmallInferredResponse 4271 ops/sec (±1.94%) 4838 ops/sec (±0.54%) 1.13
getResponse Collection 7172 ops/sec (±3.78%) 8037 ops/sec (±2.20%) 1.12
get Collection 5402 ops/sec (±0.37%) 6524 ops/sec (±0.49%) 1.21
get Query-sorted 8124 ops/sec (±3.88%) 7759 ops/sec (±5.02%) 0.96
setLong 715 ops/sec (±2.15%) 737 ops/sec (±0.35%) 1.03
setLongWithMerge 406 ops/sec (±1.80%) 415 ops/sec (±1.86%) 1.02
setLongWithSimpleMerge 435 ops/sec (±0.85%) 443 ops/sec (±1.35%) 1.02
setSmallResponse 500x 1549 ops/sec (±0.38%) 1611 ops/sec (±1.41%) 1.04
setMany 50x one-per-row 207 ops/sec (±0.50%) 220 ops/sec (±1.51%) 1.06
setMany 50 batch 5849 ops/sec (±0.40%) 6279 ops/sec (±0.55%) 1.07
setMany 500x one-per-row 20.94 ops/sec (±0.40%) 22.67 ops/sec (±0.44%) 1.08
setMany 500 batch 2534 ops/sec (±2.64%) 2821 ops/sec (±0.35%) 1.11

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

- depKey lives in WeakDependencyMap, its only user
- unvisitEntity handles all non-object entities in one block
- WeakDependencyMap tests null and INVALID dependencies directly
- concurrent-memo test helpers: updateKey, versionsOf(state)
- AGENTS.md: invariant 1 without implementation names; MAX_ENTITY_DEPTH
- memo-invariants rule also applies to normalizr schemas

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entity classes are functions, and buildQueryKey records them as the
first dependency. depKey gave them a stand-in in a module-level Map,
which kept every queried schema alive. Functions are valid WeakMap keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

- depKey's stand-in Map already covers undefined, so UNDEF.ts goes
- unvisitEntity: no value closure; isBlankPk shared by both branches
- tests: rows hold the store table; one settle() for held requests
- memo-invariants rule just points at AGENTS.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every 4th nested BuildTypeDescription in the project list is missing from
the store, null, or INVALID, cold and withCache. Each is now a memo
dependency of the project that nests it. Export INVALID from the bundle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ntucker

ntucker commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: dc95623b68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

depKey runs on every memo dependency. Looking up undefined, null and
INVALID in a Map made cached reads of lists with missing, null or INVALID
children up to 33% slower than master; fixed objects need no lookup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A missing, null or INVALID child only needs its dependency recorded; its
value is not worth caching. Skip the entity cache (closure, cache entry,
arrays) and push the dependency directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ntucker

ntucker commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 595ead97a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

This branch was successfully deployed

1 active deployment
docs-preview — 595ead97 Deployed Oct 26, 2026 by ntucker via preview / docs deploy #4136
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