refactor(execution): track pending work#4657
Merged
Merged
Conversation
|
@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel. A member of the Team first needs to authorize it. |
1d6170c to
9879a46
Compare
canonize async work tracking so promises started during execution are still observed after early errors or abort paths add `trackPromise` to resolve info async helpers so resolver-adjacent code can register fire-and-forget work for rejection safety in defaultTypeResolver, use `trackPromise(...)` for started `isTypeOf` promises in synchronous early-return and synchronous-throw paths, while still awaiting all results in the async path
yaacovCR
added a commit
that referenced
this pull request
Apr 12, 2026
Cancelled async work may still be running even after the result returns. This hook allows interested execution harnesses to track when this async work completes. Depends on: - #4657 The hook is available through `hooks.asyncWorkFinished` on execution args. An `execute` wrapper that waits for all tracked async work can be written as: ```ts function executeAndWaitForAsyncWorkFinished( args: ExecutionArgs, ): PromiseOrValue<ExecutionResult> { let hookHasFired = false; const { promise: hookFinished, resolve: resolveHookFinished } = Promise.withResolvers<void>(); const userAsyncWorkFinishedHook = args.hooks?.asyncWorkFinished; const result = execute({ ...args, hooks: { ...args.hooks, asyncWorkFinished(info) { try { userAsyncWorkFinishedHook?.(info); } finally { hookHasFired = true; resolveHookFinished(); } }, }, }); return hookHasFired ? result : hookFinished.then(() => result); } ``` To ensure resolver-side async work is also tracked and awaited, use `info.getAsyncHelpers().track(...)` or `info.getAsyncHelpers().promiseAll(...)`. `promiseAll(...)` is optimized for the common case where the returned promise is awaited (or returned) from the resolver. It only starts tracking on rejection, and does so as a side-effect. Un-awaited async side effects are an anti-pattern: ```ts resolve(_source, _args, _context, info) { const { promiseAll } = info.getAsyncHelpers(); promiseAll([Promise.reject(new Error('bad')), pendingCleanup]).catch( () => undefined, ); return 'ok'; } ``` In that anti-pattern, tracking starts only after rejection (on a later microtask), so this work is not guaranteed to delay `hooks.asyncWorkFinished`. Use `track(...)` for un-awaited async side effects: ```ts resolve(_source, _args, _context, info) { const { track } = info.getAsyncHelpers(); track([doCleanupAsync().catch(() => undefined)]); return 'ok'; } ```
yaacovCR
added a commit
that referenced
this pull request
Apr 22, 2026
Zombie code after #4657 made this.abort always sync.
yaacovCR
added a commit
that referenced
this pull request
May 7, 2026
## v17.0.0-beta.0 (2026-05-07) #### Breaking Change 💥 * [#4634](#4634) Use Object.create(null) over {} to avoid prototype issues ([@benjie](https://github.com/benjie)) * [#4700](#4700) Demote createSourceEventStream to helper taking ValidatedExecutionArgs ([@yaacovCR](https://github.com/yaacovCR)) * [#4703](#4703) rename executeQueryOrMutationOrSubscriptionEvent to executeRootSelectionSet ([@yaacovCR](https://github.com/yaacovCR)) * [#4708](#4708) fix(variables): treat undefined as absent with respect to variables ([@yaacovCR](https://github.com/yaacovCR)) * [#4710](#4710) fix: ignore undefined-valued unknown fields in input objects ([@yaacovCR](https://github.com/yaacovCR)) #### New Feature 🚀 * [#4658](#4658) feat(execution): expose asyncWorkFinished execution hook ([@yaacovCR](https://github.com/yaacovCR)) * [#4674](#4674) feat(execution): expose partial result on abort errors ([@yaacovCR](https://github.com/yaacovCR)) * [#4701](#4701) export validateExecutionArgs helper ([@yaacovCR](https://github.com/yaacovCR)) * [#4702](#4702) feat: add ValidatedSubscriptionArgs and validateSubscriptionArgs ([@yaacovCR](https://github.com/yaacovCR)) #### Bug Fix 🐞 * [#4637](#4637) refactor(queue): replace stopped promise with onStop handlers ([@yaacovCR](https://github.com/yaacovCR)) * [#4641](#4641) refactor(incremental): close stream iterator only on abnormal stop ([@yaacovCR](https://github.com/yaacovCR)) * [#4642](#4642) fix(incremental): await async incremental cleanup ([@yaacovCR](https://github.com/yaacovCR)) * [#4644](#4644) fix(withConcurrentAbruptClose): do not close unnecessarily ([@yaacovCR](https://github.com/yaacovCR)) * [#4645](#4645) fix(cancellablePromise): handle rejection when cancelling already-aborted promise ([@yaacovCR](https://github.com/yaacovCR)) * [#4643](#4643) fix(execute): handle list promise rejections ([@yaacovCR](https://github.com/yaacovCR)) * [#4646](#4646) fix(execute): handle defaultTypeResolver promise rejections ([@yaacovCR](https://github.com/yaacovCR)) * [#4648](#4648) refactor(executor): separate finish/abort orchestration ([@yaacovCR](https://github.com/yaacovCR)) * [#4655](#4655) fix(execution): finish executors before publishing responses ([@yaacovCR](https://github.com/yaacovCR)) * [#4661](#4661) fix: bubbling sync errors need not become async ([@yaacovCR](https://github.com/yaacovCR)) * [#4663](#4663) fix(executor): let aborted async paths reach finish ([@yaacovCR](https://github.com/yaacovCR)) * [#4657](#4657) refactor(execution): track pending work ([@yaacovCR](https://github.com/yaacovCR)) * [#4664](#4664) fix(perf): hoist error creation ([@yaacovCR](https://github.com/yaacovCR)) * [#4665](#4665) execution: reduce parent executor retention in incremental callbacks ([@yaacovCR](https://github.com/yaacovCR)) * [#4671](#4671) fix(valueFromAST): forward port #4652 ([@yaacovCR](https://github.com/yaacovCR)) * [#4672](#4672) fix(execution): convert all promise-like results to promises ([@yaacovCR](https://github.com/yaacovCR)) * [#4518](#4518) Fix TypeInfo.getInputType() for custom scalar list literals. ([@yuchenshi](https://github.com/yuchenshi)) * [#4692](#4692) fix incremental label null validation ([@jbellenger](https://github.com/jbellenger)) * [#4711](#4711) fix(coerceInputLiteral): null variable input object fields should override defaults ([@yaacovCR](https://github.com/yaacovCR)) * [#4712](#4712) fix: name fragment variables as such in execution errors ([@yaacovCR](https://github.com/yaacovCR)) * [#4714](#4714) fix: enhance runtime invalid default value error messages ([@yaacovCR](https://github.com/yaacovCR)) * [#4715](#4715) unify OneOf non-null and count error messages ([@yaacovCR](https://github.com/yaacovCR)) * [#4716](#4716) fix(OneOf): fail coercion when two fields are provided "pre-coercion-only" ([@yaacovCR](https://github.com/yaacovCR)) * [#4717](#4717) fix(valueFromAST): reject unknown input object fields ([@yaacovCR](https://github.com/yaacovCR)) * [#4718](#4718) fix(OneOf): count only known fields for one-of validation ([@yaacovCR](https://github.com/yaacovCR)) * [#4719](#4719) fix(OneOf): fail coercion for OneOf is the single field is invalidly provided by a default ([@yaacovCR](https://github.com/yaacovCR)) #### Polish 💅 <details> <summary> 19 PRs were merged </summary> * [#4635](#4635) polish: remove a few superfluous awaits ([@yaacovCR](https://github.com/yaacovCR)) * [#4636](#4636) allow forwarding of cancellation reasons within computations/queues ([@yaacovCR](https://github.com/yaacovCR)) * [#4638](#4638) test(queue): cover async onStop cleanup ([@yaacovCR](https://github.com/yaacovCR)) * [#4639](#4639) refactor(computation): rename cancel to abort ([@yaacovCR](https://github.com/yaacovCR)) * [#4640](#4640) refactor(workqueue): propagate async computation abort cleanup ([@yaacovCR](https://github.com/yaacovCR)) * [#4647](#4647) refactor(executor): rename finished state to aborted ([@yaacovCR](https://github.com/yaacovCR)) * [#4649](#4649) refactor(cancellablePromise): extract withCancellation ([@yaacovCR](https://github.com/yaacovCR)) * [#4650](#4650) refactor(executor): replace internal abort signal with lightweight version ([@yaacovCR](https://github.com/yaacovCR)) * [#4656](#4656) refactor(execution): introduce shared execution context ([@yaacovCR](https://github.com/yaacovCR)) * [#4653](#4653) fix: avoid prototype-colliding names in execution values ([@abishekgiri](https://github.com/abishekgiri)) * [#4673](#4673) refactor(execution): separate response construction from finish checks ([@yaacovCR](https://github.com/yaacovCR)) * [#4675](#4675) polish(execution): remove obsolete abort async checks ([@yaacovCR](https://github.com/yaacovCR)) * [#4686](#4686) refactor(benchmark): extract shared mean stats helper ([@yaacovCR](https://github.com/yaacovCR)) * [#4687](#4687) polish(benchmark): handle theoretical stats errors ([@yaacovCR](https://github.com/yaacovCR)) * [#4693](#4693) polish: add expectToThrow test helper ([@yaacovCR](https://github.com/yaacovCR)) * [#4694](#4694) polish: add expectPromise.toReject helper ([@yaacovCR](https://github.com/yaacovCR)) * [#4695](#4695) polish: clean up stream tests ([@yaacovCR](https://github.com/yaacovCR)) * [#4696](#4696) polish: introduce spy test helper ([@yaacovCR](https://github.com/yaacovCR)) * [#4704](#4704) refactor: add executionMode/serially argument to executeRootSelectionSet ([@yaacovCR](https://github.com/yaacovCR)) </details> #### Internal 🏠 <details> <summary> 17 PRs were merged </summary> * [#4676](#4676) chore(deps): upgrade to ts v6 ([@yaacovCR](https://github.com/yaacovCR)) * [#4677](#4677) internal: refactor benchmark runner structure ([@yaacovCR](https://github.com/yaacovCR)) * [#4678](#4678) internal: run benchmarks through worker files ([@yaacovCR](https://github.com/yaacovCR)) * [#4679](#4679) internal: read benchmark names through dedicated worker ([@yaacovCR](https://github.com/yaacovCR)) * [#4680](#4680) internal: separate benchmark timing and memory sampling ([@yaacovCR](https://github.com/yaacovCR)) * [#4681](#4681) fix(benchmark): use node flags only for memory sampling ([@yaacovCR](https://github.com/yaacovCR)) * [#4682](#4682) internal: use mitata for benchmark timing ([@yaacovCR](https://github.com/yaacovCR)) * [#4683](#4683) refactor(benchmark): integrate run + sampling files ([@yaacovCR](https://github.com/yaacovCR)) * [#4684](#4684) benchmark: perform timing tests in rounds to reduce variance ([@yaacovCR](https://github.com/yaacovCR)) * [#4685](#4685) benchmark: stop timing after stable pairwise comparisons ([@yaacovCR](https://github.com/yaacovCR)) * [#4688](#4688) benchmark: cache revision archives ([@yaacovCR](https://github.com/yaacovCR)) * [#4689](#4689) benchmark: report paired benchmark comparisons ([@yaacovCR](https://github.com/yaacovCR)) * [#4690](#4690) benchmark: add async object field benchmark ([@yaacovCR](https://github.com/yaacovCR)) * [#4697](#4697) internal: add runtime option to benchmark ([@yaacovCR](https://github.com/yaacovCR)) * [#4713](#4713) benchmark: add benchmark for schema validation ([@yaacovCR](https://github.com/yaacovCR)) * [#4722](#4722) internal: npm release:prepare should set publishTag ([@yaacovCR](https://github.com/yaacovCR)) * [#4723](#4723) fix(changelog): ignore open associated PRs ([@yaacovCR](https://github.com/yaacovCR)) </details> #### Committers: 5 * Abishek Kumar Giri([@abishekgiri](https://github.com/abishekgiri)) * Benjie([@benjie](https://github.com/benjie)) * James Bellenger([@jbellenger](https://github.com/jbellenger)) * Yaacov Rydzinski ([@yaacovCR](https://github.com/yaacovCR)) * Yuchen Shi([@yuchenshi](https://github.com/yuchenshi))
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.
canonize async work tracking so promises started during execution are still observed after early errors or abort paths
route defaultTypeResolver through tracked async helpers to avoid dropping pending isTypeOf promise rejections
motivation: