Skip to content

fix(apollo-forest-run): tolerate divergent null/object node chunks during update#686

Draft
vladar wants to merge 10 commits into
mainfrom
vladar/forestrun-updateobject-null-regression
Draft

fix(apollo-forest-run): tolerate divergent null/object node chunks during update#686
vladar wants to merge 10 commits into
mainfrom
vladar/forestrun-updateobject-null-regression

Conversation

@vladar

@vladar vladar commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Relaxes an overly strict invariant that caused Invariant violation during cache.write:

Invariant violation: Failed to update "<query>" at path <field>: expected CompositeNull, got ObjectDifference
Invariant violation: Failed to update "<query>" at path 0: expected CompositeUndefined, got ObjectDifference

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, which updateAffectedTreesupdateTreeresolveAffectedChunks applies to every chunk of the node. On the null/missing chunk, updateValue hits assert(isObjectValue(base)) / assert(isCompositeListValue(base)) and throws. The pre-existing "inconsistent state" guard only covered missing field values (isMissingValue on undefined) — not explicit nulls, and not list items at all (list items reach updateValue via updateCompositeListValue without any guard).

Fix

Consolidated the tolerance into updateValue: when an ObjectDifference/CompositeListDifference is applied to a base that is CompositeNull or CompositeUndefined, the chunk is skipped (left unchanged) and a descriptive warn is logged — for both field and list-item locations. Genuine type mismatches (leaf null, scalars, complex scalars) still assert.

The warn is at least as descriptive as the previous invariant and adds actionable advice, e.g.:

Skipping update for "<query>" at path <field> (in <Type>): field is an explicit null
(CompositeNull) in this chunk, but the incoming ObjectDifference expects a composite
object; the node has divergent chunks for this field (null in one, object in another).
Leaving the null value unchanged. This usually means the same node was written with
conflicting values in a single operation (e.g. aliased selections or a repeated node id)
or via a partial/errored write; ensure such selections resolve this field consistently.

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-CompositeNull test 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 (public ForestRun API 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) when findParent throws.

Validation

  • yarn test:own — 25 suites / 1099 tests pass
  • yarn types — clean
  • yarn lint — 0 errors (only pre-existing warnings)

vrazuvaev and others added 2 commits July 3, 2026 15:35
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>
@vladar vladar changed the title test: add failing regression tests for updateObject null-chunk invariant fix(forest-run): tolerate divergent null/object node chunks during update Jul 4, 2026
vrazuvaev and others added 3 commits July 4, 2026 14:33
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>
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

📊 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>
@vladar vladar changed the title fix(forest-run): tolerate divergent null/object node chunks during update fix(apollo-forest-run): tolerate divergent null/object node chunks during update Jul 7, 2026
vrazuvaev and others added 2 commits July 10, 2026 18:22
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>
vrazuvaev and others added 2 commits July 13, 2026 15:31
…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>
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.

1 participant