Skip to content

🤖🤖🤖 perf: defer MissingFieldError construction and avoid JSON.stringify in cache diff - #13329

Merged
jerelmiller merged 13 commits into
apollographql:release-4.3from
AmariahAK:main
Jul 23, 2026
Merged

🤖🤖🤖 perf: defer MissingFieldError construction and avoid JSON.stringify in cache diff#13329
jerelmiller merged 13 commits into
apollographql:release-4.3from
AmariahAK:main

Conversation

@AmariahAK

@AmariahAK AmariahAK commented Jul 12, 2026

Copy link
Copy Markdown

Fixes #13305.

Problem

Two expensive operations run eagerly on the cache-diff hot path even when the caller only checks diff.complete (a cheap boolean):

  1. JSON.stringify(objectOrReference, null, 2) — pretty-prints the full parent object for every missing-field message, scaling with object size.
  2. new MissingFieldError(...) — constructs an Error subclass (paying V8 stack capture) per incomplete diff, even though diff.missing is only consumed in __DEV__-guarded logging on most call paths.

Solution

  • Cheap messages: Replaced the JSON.stringify with a __typename lookup. Embedded parents now produce "object Profile" instead of the full pretty-printed object dump.
  • Lazy errors: diffQueryAgainstStore now derives diff.complete from the raw execResult.missing tree (a MissingTree). The MissingFieldError is built lazily via a getter, only when diff.missing is actually accessed. The getter caches the result so repeated access is cheap.
    All three consumers of diff.missing are safe — two are __DEV__-guarded, one is a legitimate watchFragment consumer. The hot broadcastWatch path only compares diff.result with equal(), so the getter is never triggered by property enumeration.

Changes

File Change
src/cache/inmemory/readFromStore.ts Cheap missing-field message + lazy MissingFieldError getter
src/cache/inmemory/__tests__/readFromStore.ts 2 new tests + 3 existing assertions updated
src/cache/inmemory/__tests__/diffAgainstStore.ts 1 assertion updated
src/cache/inmemory/__tests__/policies.ts 1 assertion updated
.changeset/lazy-diff-diagnostics.md Patch-level changeset

Checklist:

  • Includes a changeset
  • Significant new logic is covered by tests
  • New feature (not applicable — performance fix for existing behavior)

Summary by CodeRabbit

  • Performance

    • Improved cache diff performance by avoiding unnecessary serialization of stored objects.
    • Missing-field diagnostics are now generated only when requested.
  • Bug Fixes

    • Improved missing-field error messages with shorter, more relevant object descriptions.
    • Cache completeness is now reported accurately without requiring full error construction.

…n cache diff

Derive diff.complete from the raw missing-field tree instead of eagerly
constructing a MissingFieldError (which extends Error and pays V8 stack
capture). The error is now built lazily via a getter, only when
diff.missing is accessed.

Replace the pretty-printed JSON.stringify(objectOrReference, null, 2)
in missing-field messages with a cheap __typename lookup, avoiding
O(object-size) cost per missing field on embedded parents.

Fixes apollographql#13305.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@apollo-cla

Copy link
Copy Markdown

@AmariahAK: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/

@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4358da5

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

This PR includes changesets to release 1 package
Name Type
@apollo/client 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

@apollo-librarian

apollo-librarian Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ddcd2ef-e6ae-4bac-800d-1eeba3f7c348

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

diffQueryAgainstStore now computes completeness without constructing MissingFieldError, creates the error only when diff.missing is accessed, and uses shorter missing-field descriptions. Tests and a changeset document and validate the updated behavior.

Changes

Cache diff diagnostics

Layer / File(s) Summary
Lazy diff runtime behavior
src/cache/inmemory/readFromStore.ts
Completeness is derived from the raw missing tree, MissingFieldError construction is deferred behind a cached getter, and missing-field messages use references, typenames, or a generic object label instead of serialized store objects.
Diagnostic validation and release note
src/cache/inmemory/__tests__/*, .changeset/lazy-diff-diagnostics.md
Existing diagnostic expectations are shortened, new tests verify lazy construction and compact messages, and the patch changeset records the behavior.
Estimated code review effort: 3 (Moderate) ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CacheDiff
  participant StoreReader
  participant SelectionSet
  participant DiffResult
  CacheDiff->>StoreReader: request diff
  StoreReader->>SelectionSet: execute selection set
  SelectionSet-->>StoreReader: result and raw missing tree
  StoreReader-->>CacheDiff: complete status
  CacheDiff->>DiffResult: access missing diagnostics
  DiffResult->>StoreReader: construct MissingFieldError lazily
Loading

Suggested reviewers: phryneas

Poem

A rabbit watched the cache run light,
No giant strings to slow its flight.
Missing clues wait in a neat little queue,
Until diff.missing asks them to.
“Hop faster, errors—when called, appear!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements #13305 by deferring MissingFieldError creation, avoiding object stringification, and preserving diff.missing diagnostics.
Out of Scope Changes check ✅ Passed The changes stay focused on cache-diff diagnostics and matching tests/changeset, with no unrelated scope added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: lazy MissingFieldError construction and removing JSON.stringify from cache diff diagnostics.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@AmariahAK

Copy link
Copy Markdown
Author

hi @apollo-cla i already signed, kindly take a look

@jerelmiller

Copy link
Copy Markdown
Member

Hey @AmariahAK 👋

Appreciate the contribution. A few bits of feedback:

  1. I'd like to get some benchmarks on how this change affects the overall performance. Thanks to @wolfie in Incomplete cache diffs eagerly build missing-field diagnostics (JSON.stringify of store objects + MissingFieldError construction) in production #13305, you should be able to use that prewritten benchmark here to help. What I'd like to understand is how each changed piece affects the overall performance (@wolfie did some of this already, but its helpful to see with this actually implemented).
  2. I don't think __typename is a sufficient replacement for JSON.stringify because you lose too much information here. Take the following query:
query {
  user {
    id
    name
    bestFriends {
      id
      name
    }
  }
}

where user returns a Person object and bestFriends returns an array of Person objects. JSON.stringify provides enough information to disambiguate between which of the Person objects the missing field applies to. Without it, you're stuck having to use devtools or cache extraction to determine exactly what part of the query is missing data. With a very large query, or with lots of cache written to the data, this can add quite a bit of complexity to debugging. With just __typename, its impossible to tell which of these objects are missing fields.

I'm ok not blindly using JSON.stringify, especially if we have a faster option, but I'd like a solution that doesn't remove so much information (this is why the benchmarks are important so we can figure out how best to balance the two).

For data, we have 1 of 3 "types" of objects that can be included in that message:

  • Reference objects (the { __ref: "Person:1" } style objects that point to another cache record)
  • Normalized objects (an object which has its keyFields included... typically id)
  • Non-normalized objects

For normalized objects, I'd like to experiment with replacing the full stringified object with its cache identity (e.g. cache.identify(normalizedObject)). That should provide enough information to quickly debug while avoiding JSON.stringify.

For non-normalized objects, I think we have to keep JSON.stringify, otherwise debugging becomes very difficult.

For reference objects, I'm ok leaving as-is. JSON.stringify should be cheap since its a single property.

Note these proposed solutions are not exhaustive ideas, but rather the things I could think of that maintains debuggability without the need for JSON.stringify. What we need is to check that against benchmarks to see if we actually see perf improvement or not here (e.g. is JSON.stringify or cache.identify cheaper?) and to weigh that against the current baseline.

I think lazy initializing the error probably helps most of the issue, but again, let's see how much we gain/lose by avoiding JSON.stringify. If its negligible enough, I'd prefer keeping it.


By the way, in the future, its really helpful if you talk through ideas in the issue beforehand rather than pointing your agent at a PR and implementing verbatim against a suggested fix without our input. It reduces churn and helps both us and you avoid unnecessary implementation/review on something that ultimately might not work.

Thanks!


PS - I'd kindly ask that you talk to us as a human for additional communication on this PR rather than just having your agent blindly add commits and respond for you. I'd like your personal feedback on how changes here affect both performance and debuggability because I think that needs some personal taste. That also helps me give you a green light on what works best to solve the underlying issue. Thanks!

@jerelmiller

Copy link
Copy Markdown
Member

Oh I also forgot to mention... I'd love if we can test with #13324 as well. I'm doing some work in that PR (which will be released in 4.3) that currently does more with diff.missing. Its a hot path and might negate some of the work we do here, so I think it would be useful to see how much my PR affects any changes here 🙂

@jerelmiller jerelmiller added the 🏓 awaiting-contributor-response requires input from a contributor label Jul 13, 2026
@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller apologies, for seeing this late, let me do these changes

@jerelmiller

Copy link
Copy Markdown
Member

No problem, thanks!

FYI, I'm working on this branch which is doing a lot of work in readFromStore to better handle incremental responses. There is a good chance both of our branches will conflict. #13324 ended up being a stop-gap solution that only solves part of the issue. I found other cases it doesn't handle and making those changes in the cache ended up being the more optimal approach.

Just wanted to note that since I will likely hold off on merging anything here until I get that work done so that I can test it cleanly with anything in this branch. That also means this change will almost certainly land in 4.3 🙂. Just wanted you to be aware so that you can keep an eye on that branch to possibly test against (though its in a pretty broken state right now 😬)

Use cache.identify() for normalized objects to provide unique,
disambiguated cache IDs in missing-field error messages while
restoring JSON.stringify for non-normalized (embedded) objects
to preserve full debuggability. The lazy MissingFieldError
construction (the primary perf fix) is unchanged.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller i dont think it conflicts, ive made the changes youve requested, could you take a look?

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@jerelmiller

Copy link
Copy Markdown
Member

Do you have some benchmarks I can review to understand how the different approaches affect performance? For example, is cache.identify any faster than JSON.stringify, etc?

@AmariahAK

AmariahAK commented Jul 15, 2026

Copy link
Copy Markdown
Author

@jerelmiller
For Benchmarks — I ran a microbenchmark comparing the different message-building approaches on Node.js v25.7.0 (unthrottled). Results in milliseconds per call (100 iterations after warmup):

 Message-building (per missing field):
   JSON.stringify(small normalized, 4 fields)       0.0003 ms
   JSON.stringify(medium embedded, 6 fields)         0.0003 ms  
   JSON.stringify(large embedded, ~8KB)              0.0039 ms   (13x cost)
   __typename lookup only                            0.0000 ms
 
 cache.identify() vs JSON.stringify:
   cache.identify(small normalized)                  0.0008 ms
   cache.identify(medium embedded)                   0.0005 ms
   cache.identify(large embedded)                    0.0005 ms
   JSON.stringify(small normalized)                  0.0002 ms
 
 MissingFieldError:
   new MissingFieldError(...)                        0.0016 ms
 
 Full end-to-end (300 diffs, repo from #13305):
   Total                                             10.2 ms    (0.034 ms/diff)

Takeaways:

  1. cache.identify() is slightly slower than JSON.stringify for small normalized objects (0.0008 vs 0.0002 ms). This makes sense — identify has to read __typename, look up type policies, extract key fields — while JSON.stringify of a 4-field object is nearly free. However, both are sub-microsecond and the difference is negligible compared to the MissingFieldError construction cost (0.0016 ms).

  2. JSON.stringify cost scales with object size — 0.0003 ms for a small object vs 0.0039 ms for an 8KB object (~13x). This is why the original issue measured real-world impact: with many large embedded parents on the hot diff path, these micro-costs add up.

  3. The lazy MissingFieldError construction is the dominant win. At 0.0016 ms per construction, deferring this until diff.missing is actually read saves the most time on the hot path where callers only check diff.complete.

  4. The tiered message approach is the right tradeoff. For normalized objects, cache.identify() costs 0.0008 ms (still cheap) but provides disambiguated cache IDs ("Customer:c1 object" vs just "object Customer"). For non-normalized objects, JSON.stringify is the only option that preserves debuggability — and it's only paid when diff.missing is actually read (since the message string is part of the MissingFieldError, which is now lazily constructed).

I can add the benchmark to the pr if youd like

@jerelmiller

Copy link
Copy Markdown
Member

Thats helpful thanks! I'm not as worried about the more expensive cache.identify on smaller objects since those should run fast anyways due to their size. The difference there is likely negligible.

If I'm looking at this correctly, in the worst case (all objects are non-normalized), we would actually have a slight perf regression because now we're paying the cost of cache.identify + JSON.stringify correct?

That said, from a taste perspective, I'd love if you could give me some samples of what the new error message looks like with these scenarios (it would help to log the error.path as well since error.message is just the first string message it can find in path).

  • all normalized
  • all non-normalized
  • mixed normalized + normalized

@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller
In the worst case (all non-normalized objects) we now pay cache.identify() + JSON.stringify() instead of just JSON.stringify. But cache.identify() for a non-normalized object returns undefined almost instantly (0.0005ms) — it just checks __typename and finds no key fields. The overhead is negligible compared to the JSON.stringify cost itself (0.0039ms for an 8KB object, ~13x the regression), and even more so compared to the old eager MissingFieldError construction (0.0016ms per diff). Since the error is now lazy, the message strings aren't built on the hot path anyway.

For the error message examples:
Re: message samples — here's what each scenario produces:

Scenario 1: All normalized objects

error.message: Can't find field 'name' on Person:1 object

error.path: {
  "user": {
    "name": "Can't find field 'name' on Person:1 object",
    "bestFriends": "Can't find field 'bestFriends' on Person:1 object"
  },
  "otherUser": {
    "name": "Can't find field 'name' on Person:2 object"
  }
}

Each Person is disambiguated by cache ID — Person:1 vs Person:2 makes it immediately clear which part of the query is missing data.

Scenario 2: All non-normalized (keyFields: false)

error.message: Can't find field 'id' on object {
  "__typename": "Person",
  "name": "Alice"
}

error.path: {
  "user": {
    "id": "Can't find field 'id' on object {\n  \"__typename\": \"Person\",\n  \"name\": \"Alice\"\n}",
    ...
  },
  "otherUser": {
    "id": "Can't find field 'id' on object {\n  \"__typename\": \"Person\",\n  \"name\": \"Bob\"\n}",
    ...
  }
}

Full JSON dump — you can see the object's contents to distinguish user (Alice) from otherUser (Bob) even though both are Person.

Scenario 3: Mixed normalized + non-normalized

error.message: Can't find field 'id' on object {
  "bestFriends": [
    { "__typename": "Friend", "name": "Charlie" }
  ]
}

error.path: {
  "user": {
    "id": "Can't find field 'id' on object { ... full user object ... }",
    "name": "Can't find field 'name' on object { ... full user object ... }",
    "bestFriends": {
      "0": {
        "id": "Can't find field 'id' on object {\n  \"__typename\": \"Friend\",\n  \"name\": \"Charlie\"\n}"
      }
    }
  },
  "otherUser": {
    "name": "Can't find field 'name' on Person:{\"id\":\"2\"} object"
  }
}

Three different message formats at play here:

  • user is non-normalized (embedded) → full JSON dump with bestFriends array visible
  • bestFriends[0] is a non-normalized Friend → full JSON dump
  • otherUser is normalized → cache identity Person:{"id":"2"} object

Scenario 4: Reference object

error.message: Can't find field 'missingField' on Person:1 object

error.path: {
  "user": {
    "missingField": "Can't find field 'missingField' on Person:1 object"
  }
}

References unchanged — still Person:1 object (cheap, unique).

@jerelmiller

Copy link
Copy Markdown
Member

Ok awesome, I think that maintains enough information and am happy with that.

@wolfie would you mind looking at the comment above to make sure it retains enough information?

@AmariahAK let me noodle on whether to include these changes in the upcoming 4.3 release, or as a patch in 4.2.x. I'll talk with the team to see if the change in the missing message is enough to count for a minor release or not. I should have an answer for you soon and will do a full review of the code at that time. Thanks!

@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller
Sounds great, Thanks for the thoughtful feedback throughout!

@wolfie

wolfie commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@wolfie would you mind looking at the comment above to make sure it retains enough information?

Sorry, I've had some busy times so I haven't had the chance to put proper effort into this. However, my issue hasn't really been the clarity of the error messages, but rather that with React, a lot of the queries are re-evaluated as partial during re-rendering causing the construction of heavy error messages that are ultimately thrown away and never shown to the user (since once the state is stable, eventually the queries evaluate properly)

So, in short, I'm okay with the error messages as proposed, but I'm not sure I'm a representative opinion, if that makes sense :).

@jerelmiller
jerelmiller changed the base branch from main to release-4.3 July 18, 2026 04:52
@jerelmiller

Copy link
Copy Markdown
Member

@wolfie no worries! I mostly just wanted to make sure you didn't have any concerns there. Thanks!

@jerelmiller jerelmiller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Appreciate the contribution!

The plan is to get this in the next minor (4.3). I'd love to get this merged sooner than later so that I don't have to burden you with all the merge conflicts once I'm finished with the work on my current branch 🙂.

Comment thread src/cache/inmemory/__tests__/diffAgainstStore.ts
Comment thread src/cache/inmemory/__tests__/policies.ts Outdated
Comment thread src/cache/inmemory/__tests__/readFromStore.ts
Comment thread src/cache/inmemory/__tests__/readFromStore.ts Outdated
Comment thread src/cache/inmemory/__tests__/readFromStore.ts Outdated
Comment thread src/cache/inmemory/readFromStore.ts Outdated
Comment thread src/cache/inmemory/__tests__/readFromStore.ts Outdated
Comment thread src/cache/inmemory/readFromStore.ts Outdated
Comment thread src/cache/inmemory/readFromStore.ts Outdated
Comment thread .changeset/lazy-diff-diagnostics.md Outdated
AmariahAK and others added 2 commits July 18, 2026 09:08
- Revert unnecessary test reformats in diffAgainstStore.ts and policies.ts
  (back to inline JSON.stringify where message format didn't change)
- Remove unnecessary 'as StoreObject' cast in readFromStore.ts
- Use consistent 'object {id}' format for identified-object messages
- Fix falsy fallback to produce 'object {}' instead of 'object'
- Restructure lazy-semantics test to use single toEqual assertion
- Rewrite changeset to focus on performance impact (natural tone)

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller
All set , pushed the fixes. The changeset now pulls the performance context from the original issue (the ~95–140ms main-thread waste, 90ms in the constructor alone, the sibling-watch-dirtying scenario). Also addressed the other feedback: reverted the unnecessary test reformats in diffAgainstStore.ts and policies.ts, dropped the as StoreObject cast, fixed the message format for identified objects, and restructured that lazy-semantics test to use a single toEqual assertion like the rest of the file does. Ready for another look whenever you get to it.

@AmariahAK
AmariahAK requested a review from jerelmiller July 18, 2026 06:35
)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/stale](https://redirect.github.com/actions/stale) | action |
minor | `v10.3.0` → `v10.4.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](..apollographql/issues/11062) for more information.

---

### Release Notes

<details>
<summary>actions/stale (actions/stale)</summary>

###
[`v10.4.0`](https://redirect.github.com/actions/stale/releases/tag/v10.4.0)

[Compare
Source](https://redirect.github.com/actions/stale/compare/v10.3.0...v10.4.0)

#### What's Changed

##### Bug Fix

- Fixed `only-issue-types` validation by
[@&apollographql#8203;trueberryless](https://redirect.github.com/trueberryless) in
[#&apollographql#8203;1338](https://redirect.github.com/actions/stale/pull/1338)

##### Dependency Updates

- Bump undici to 6.27.0 via override, clean up stale license files, and
version to 10.4.0. by
[@&apollographql#8203;dependabot](https://redirect.github.com/dependabot) in
[#&apollographql#8203;1342](https://redirect.github.com/actions/stale/pull/1342)

#### New Contributors

- [@&apollographql#8203;trueberryless](https://redirect.github.com/trueberryless)
made their first contribution in
[#&apollographql#8203;1338](https://redirect.github.com/actions/stale/pull/1338)

**Full Changelog**:
<actions/stale@v10.3.0...v10.4.0>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/Los_Angeles)

- Branch creation
  - "every weekend"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/apollographql/apollo-client).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyI6Y2hyaXN0bWFzX3RyZWU6IGRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller sorry for the late response, but the undefined comes from custom read functions returning undefined inside arrays. execSubSelectedArrayImpl filters the array with:

array.filter((item) => item === undefined || context.store.canRead(item))

This intentionally preserves undefined items (to keep array indices stable when an entity is evicted). Then in the .map(), undefined isn't null and isn't an array, so when field.selectionSet exists it gets passed directly to executeSelectionSet as objectOrReference. The old message code did JSON.stringify(objectOrReference, null, 2) which stringifies undefined to the JSON value undefined — hence "object undefined".

The existing ducks test in readFromStore.ts exercises this path: the custom read function maps evicted references to undefined, and execSubSelectedArrayImpl propagates them. The "object {}" fallback in the current code handles it correctly, but the deeper question is whether execSubSelectedArrayImpl should be passing undefined through to executeSelectionSet at all, or short-circuiting earlier. Happy to adjust if you have a preference.

@jerelmiller i answered your q here

@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller i answered your q a while back, are there any more changes youd like me to do, or any other areas youd like to focus on?

@jerelmiller

Copy link
Copy Markdown
Member

Apologies, its been a very busy couple days for me. Hoping to have a small gap today to come back to this!

@AmariahAK

Copy link
Copy Markdown
Author

@jerelmiller no worries take your time

@jerelmiller

Copy link
Copy Markdown
Member

Ok finally got my PR in place and have some time. I will look at this first thing in the morning. Thanks for your patience!

@AmariahAK

Copy link
Copy Markdown
Author

Hey, no worries, take your time, thanks for taking time to go through this in depth.

@jerelmiller jerelmiller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution!

@jerelmiller
jerelmiller merged commit 1d581d2 into apollographql:release-4.3 Jul 23, 2026
38 checks passed
@AmariahAK

Copy link
Copy Markdown
Author

Thanks for the review and the merge

jerelmiller pushed a commit that referenced this pull request Jul 24, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to release-4.3, this
PR will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`release-4.3` is currently in **pre mode** so this branch has
prereleases rather than normal releases. If you want to exit
prereleases, run `changeset pre exit` on `release-4.3`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @apollo/client@4.3.0-alpha.4

### Patch Changes

- [#13347](#13347)
[`7d543d6`](7d543d6)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue
where `network-only` incremental queries could cause cache data to leak
into the emitted result when a `@defer` or `@stream` boundary already
had complete data in the cache. Cache data inside pending `@defer`
objects and `@stream` arrays are now pruned so that only completed
`@defer` or `@stream` boundaries are returned.

NOTE: This change only applies to `InMemoryCache` when using
`GraphQL17Alpha9Handler`.

- [#13329](#13329)
[`1d581d2`](1d581d2)
Thanks [@AmariahAK](https://github.com/AmariahAK)! - Cache diffs for
incomplete queries no longer pay the cost of building a full
`MissingFieldError` when the `missing` property is not accessed. The
error object is now only constructed when the `missing` property is
accessed the first time. This improves performance by avoiding a V8
stack capture when `missing` is ignored entirely.

As an additional small performance improvement, `JSON.stringify` is no
longer used in the error message on objects whose cache ID is known.
`JSON.stringify` is only used for non-normalized objects.

- [#13347](#13347)
[`7d543d6`](7d543d6)
Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue
where partial cache data could leak into intermediate incremental
results. This could cause runtime crashes if you relied on the presence
of values to determine whether the `@defer` data had streamed in or not.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🏓 awaiting-contributor-response requires input from a contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incomplete cache diffs eagerly build missing-field diagnostics (JSON.stringify of store objects + MissingFieldError construction) in production

5 participants