Repository navigation
Fixed mutation root fields not being executed serially - #635
Open
xperiandri wants to merge 3 commits into
Open
xperiandri wants to merge 3 commits into
xperiandri wants to merge 3 commits into
Conversation
The executor called the resolvers of all root fields up front and only awaited the resulting AsyncVals in order with AsyncVal.collectSequential. A synchronous resolver of a later mutation field therefore ran, and a task started by a later resolver kept running, while an earlier field and its nested fields were still pending, so the side effects of a mutation did not happen in document order. - Added AsyncVal.collectSequentialWith, which maps each item to an AsyncVal only once the AsyncVal of the previous item has completed, stays immediate while every item is, and stops at the first failure. - The Sequential strategy of executeQueryOrMutation now starts each root field through it; the Parallel strategy and subscriptions are unchanged. - Validation now rejects @defer on an inline fragment or fragment spread at the root of a mutation unless disabled with `if: false`: only root fields were checked, so a deferred root fragment was accepted and its root fields ran after the rest of the mutation and concurrently. - Added regression tests with task, async and synchronous resolvers and nested fields that record start and finish events behind gates the test releases, a query test showing root fields still run concurrently, and tests of AsyncVal.collectSequentialWith and of the validation rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ration A resolver can throw before it returns its value, as an AsyncField resolver throwing instead of returning its Async does. The exception escaped resolveField: for a root field it failed the whole operation with a request error, discarding the results of the mutation fields already executed and skipping the later ones, and for a nested field the resolveWith of its parent caught it, failing the parent instead. It is now a failed value of its own field, like a failure of the returned computation. Also corrected the comment and the release notes of serial execution, which do not wait for the parts of a root field deferred with @defer or streamed with @stream, and of the @defer validation at the mutation root, which still accepts a directive disabled with a literal `if: false` although the specification does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The execution and validation changes are well-scoped and comprehensively tested, with only minor non-blocking style feedback.
1 open finding
What changed in this PR
Fixes serial mutation execution and correctly scopes synchronous resolver failures to their fields.
Changes:
- Adds lazy sequential
AsyncValcollection for root fields. - Rejects deferred mutation-root fragments.
- Adds execution, validation, and failure-path tests.
| File | Description |
|---|---|
Execution.fs |
Serializes root execution and catches synchronous resolver throws. |
AsyncVal.fs |
Adds collectSequentialWith. |
Validation.fs |
Validates deferred mutation-root fragments. |
MutationTests.fs |
Tests ordering, concurrency, and resolver failures. |
AsyncValTests.fs |
Tests lazy sequential collection. |
AstValidationTests.fs |
Tests fragment validation. |
RELEASE_NOTES.md |
Documents behavior and breaking validation change. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1652
to
+1653
| usagesOf fragment.Directives [] | ||
| @ rootIncrementalDirectiveUsages fragmentDefinitions visitedFragments fragment.SelectionSet |
… in the mutation tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Results 9 files 9 suites 14m 28s ⏱️ Results for commit 239d6fa. |
This branch has not been deployed
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.

Problem
The spec requires the root fields of a mutation to be executed serially: a root field starts only once the previous one has completed. This affected mutations and any operation planned with
ExecutionStrategy.Sequential.AsyncVals in order withAsyncVal.collectSequential. So a synchronous resolver of a later field ran, and a task started by a later resolver kept running, while an earlier field and its nested fields were still pending. The side effects of a mutation did not happen in document order.@deferand@streamon mutation root fields, but not@deferon an inline fragment or fragment spread at the root. A mutation could therefore defer a fragment at its root. The root fields of that fragment then ran after the rest of the mutation, and concurrently with each other.AsyncFieldresolver does when it throws instead of returning itsAsync.resolveWithcaught the exception, so the parent failed instead of the field.Changes
AsyncVal.collectSequentialWith(new). It maps each item to anAsyncValonly once the previous item'sAsyncValhas completed. It stays immediate while every item is, and stops at the first failure.Sequentialstrategy ofexecuteQueryOrMutationstarts each root field throughcollectSequentialWith. TheParallelstrategy and subscriptions are unchanged.@deferor streamed with@streamare still delivered later, as the spec allows: the next root field does not wait for them.@deferon an inline fragment or fragment spread at the root of a mutation, as the incremental delivery spec requires (graphql-spec#1110, "Defer And Stream Directives Are Used On Valid Root Field").if: falseis still accepted, which the spec does not allow. The library already allowed it on root fields before this change.resolveFieldturns an exception thrown by a resolver before it returns into a failed value of its own field. Such a field now gets a field error, with null propagation as usual.Tests
The tests are xUnit.
MutationTests.fs. Resolvers record when they start and finish, behind gates that the test releases one at a time, so the order of events depends only on the order in which the engine starts resolvers, never on timing. The tests cover:@deferdirectives, run serially;AsyncValTests.fscoverscollectSequentialWith: the mapping order, staying immediate, and stopping after an immediate or an asynchronous failure.AstValidationTests.fscovers@deferon fragments selecting mutation root fields.The new tests fail with the previous
Execution.fsandValidation.fs.An independent review checked the following:
Async, hotTask,Define.CustomFieldandTaskSeqField.collectSequentialWith:dev.Verification:
Notes
validation-planning-hardeningboth changerootIncrementalDirectiveUsagesinValidation.fs. Whichever is merged second needs a rebase.dev. The spec says they may be cancelled; graphql-js stops there.🤖 Generated with Claude Code