fix(apollo-forest-run): tolerate divergent null/object node chunks during update#686
Draft
vladar wants to merge 10 commits into
Draft
fix(apollo-forest-run): tolerate divergent null/object node chunks during update#686vladar wants to merge 10 commits into
vladar wants to merge 10 commits into
Conversation
Two regression tests reproduce an Invariant violation (expected CompositeNull, got ObjectDifference) when a normalized node has one chunk where a field is an embedded object and another where it is explicit null. A later write that changes that field produces a single ObjectDifference that is applied to every chunk of the node, tripping assert(isObjectValue(base)) on the CompositeNull chunk. One test triggers the divergent chunks via aliases, the other via a repeated node id in a list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…vergent node chunks A normalized node can have divergent chunks for the same field - an explicit null (CompositeNull) in one chunk and an embedded object/list in another (e.g. aliased selections or a repeated node id in one write, or a partial/errored write). The single object/list difference computed for the node was applied to every chunk, so updateValue hit assert(isObjectValue(base)) on the null chunk and threw an Invariant violation. updateObjectValue now skips the null chunk (mirroring the existing inconsistent-state handling for missing fields) and logs a descriptive, actionable warning instead of crashing. The lingering null chunk is reconciled on the next write of the node. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…teobject-null-regression # Conflicts: # packages/apollo-forest-run/src/__tests__/regression.test.ts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📊 Benchmark Analysis Report✅ No significant performance changes detected Threshold: 5% change Updated: 2026-07-13T14:58:35.171Z |
…ms during update Production telemetry shows the same divergent-chunk invariant firing at list-item positions (e.g. 'at path 0: expected CompositeUndefined/CompositeNull, got ObjectDifference'), not just fields. The field-level guard did not cover list items, which reach updateValue via updateCompositeListValue without a null/missing guard. Consolidate the tolerance into updateValue: when an Object/CompositeList difference is applied to a CompositeNull or CompositeUndefined base, skip it (leave unchanged) and log a descriptive warning, for both field and list-item locations. Genuine type mismatches (leaf null, scalars, complex scalars) still assert. Adds a list-item regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an incompatible-update invariant fires on a non-addressable value (a CompositeNull/CompositeUndefined, whose source data is null/undefined), getDataPathForDebugging cannot locate it, so the message fell back to only the last field/index segment (e.g. 'value' instead of 'listContainer.items.0.value'). Thread the failing value's parent chunk (always an addressable ObjectChunk/CompositeListChunk) into the location and derive the path from the parent plus the field/index step, so the full path is reported even for non-addressable values. The enclosing (in <TypeName>) node is likewise resolved from the parent when needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ution resolveFailurePath now derives the path from a nested property (location.field.dataKey) rather than an eagerly-captured primitive, so a malformed field could throw while building the invariant message and mask the real invariant. Wrap the whole derivation in try/catch, consistent with the other best-effort diagnostic helpers (chunkPath, nodeTypeClause), so path resolution can never throw over the invariant it is reporting. Add a regression test that forces findParent to throw during both path and enclosing-node resolution and asserts the invariant message still surfaces. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ath' into vladar/forestrun-updateobject-null-regression # Conflicts: # packages/apollo-forest-run/src/forest/updateObject.ts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Relaxes an overly strict invariant that caused
Invariant violationduringcache.write:Root cause
A normalized node can end up with two chunks for the same position (a field or a list item): one where it is a real embedded object/list and another where it is an explicit
null(CompositeNull) or missing (CompositeUndefined). This happens with aliased selections, a repeated node id in a single write, or a partial/errored write.A later write produces a single
ObjectDifference/CompositeListDifference, whichupdateAffectedTrees→updateTree→resolveAffectedChunksapplies to every chunk of the node. On the null/missing chunk,updateValuehitsassert(isObjectValue(base))/assert(isCompositeListValue(base))and throws. The pre-existing "inconsistent state" guard only covered missing field values (isMissingValueon undefined) — not explicit nulls, and not list items at all (list items reachupdateValueviaupdateCompositeListValuewithout any guard).Fix
Consolidated the tolerance into
updateValue: when anObjectDifference/CompositeListDifferenceis applied to a base that isCompositeNullorCompositeUndefined, the chunk is skipped (left unchanged) and a descriptivewarnis logged — for both field and list-item locations. Genuine type mismatches (leaf null, scalars, complex scalars) still assert.The
warnis at least as descriptive as the previous invariant and adds actionable advice, e.g.:Incorporates #689 (full data path in invariant/diagnostic messages)
Merged #689, which reports the full data path (not just the last field/index) when the failing value is a non-addressable
CompositeNull/CompositeUndefined— deriving the path from the parent chunk. This benefits both the remaining genuine-mismatch invariants and the new divergent-null warning above, which now also carries the full path (e.g.listContainer.items.0.value (in Entity)). #689's message builders are wrapped defensively so path resolution never throws over the invariant being reported.Since this PR relaxes the very invariant #689's nested-
CompositeNulltest asserted, that test was updated to reflect the new behaviour: the divergent-null case no longer throws — it logs a warning that still contains the full path.Tests
regression.test.ts— three tests (publicForestRunAPI only): divergent null/object at a field via aliases, via a repeated node id in a list, and at a list-item position.updateObject.test.ts— full path is reported for a nested non-addressable value; the divergent-null warning carries the full path without throwing; and the message builder degrades gracefully (still surfaces the invariant) whenfindParentthrows.Validation
yarn test:own— 25 suites / 1099 tests passyarn types— cleanyarn lint— 0 errors (only pre-existing warnings)