Repository navigation
Sync resolvers - #3732
Open
DaddyWarbucks wants to merge 2 commits into
Open
Sync resolvers#3732DaddyWarbucks wants to merge 2 commits into
DaddyWarbucks wants to merge 2 commits into
Conversation
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.
Summary
Allow synchronous property resolvers, converters, and virtual resolvers to execute without creating unnecessary promises or introducing internal microtask boundaries.
Previously, resolver execution treated synchronous work as asynchronous at several layers:
resolveProperty()always returned a promise.Promise.all(propertyList.map(async ...)), including for properties that only needed copying.convert()always returned a promise, even when no converter was configured.virtual()wrapped every callback in anasyncfunction.These costs repeat across properties, records, and resolver stages. For small synchronous transformations, promise allocation and scheduling can account for a meaningful part of execution time.
The updated implementation handles plain values immediately and waits for asynchronous results when necessary.
Implementation
Synchronous property execution
Move property execution into a private
resolvePropertyValue()helper that can return either a value or a promise.Keep public
resolveProperty()asynchronous so existing callers can continue using.then(),.catch(), orawait.Replace the asynchronous property mapping with a loop that:
Independent asynchronous properties continue to start concurrently. Synchronous throws and asynchronous rejections continue to contribute to the existing property-error aggregation.
Preserve property overrides
Use the internal helper only when the instance uses the default public implementation:
Subclass overrides and instance replacements of
resolveProperty()continue to participate in resolution.Synchronous conversion
Move conversion logic into a private
convertValue()helper while keeping publicconvert()asynchronous.Internally,
resolve()uses the helper for the default implementation and calls overriddenconvert()methods when present:When conversion returns immediately—or no converter exists—property resolution can begin without an intervening
await. Asynchronous conversion still finishes before property resolution starts.Synchronous virtuals
Remove
asyncfrom the wrapper returned byvirtual().Synchronous virtual callbacks now return their values directly, allowing them to use the property fast path. Asynchronous callbacks continue returning promises.
The virtual marker remains intact, preserving virtual-property identification and
$selecthandling.Promise detection
Add an internal
isPromiseLike()helper to recognize native promises and thenables. Execution follows the returned value rather than relying on whether a callback was declaredasync.Compatibility and observable behavior
Public
resolve(),resolveProperty(), andconvert()continue returning promises.Property selection, omission of
undefinedvalues, circular-dependency checks, conversion status, and property-error aggregation remain in place.Two observable details change:
resolve()continue returning promises and handling property errors.This change is limited to
packages/schema/src/resolver.ts. It does not modify the hook dispatcher or service-integration hooks.Benchmark setup
Compare the resolver implementation from these revisions:
8c007bc43e30106bee7158e7f699089232e6e76521be90daf528967ad7def7dbc6ec05f30840e7bdEnvironment:
Methodology
--expose-gc, collecting garbage before each measured scenario.Before and after outputs were checked for equivalent values after canonicalizing object key order. The service workload also checked equivalent external dispatch output.
Resolver workloads
The scenarios cover:
setImmediate.The asynchronous-property scenarios use immediately fulfilled promises without database or network work.
Benchmark results
Resolver execution
Times are median microseconds per resolver call. Speedup is before time divided by after time.
setImmediateMemory-backed service
Each service call returns 100 records, each with 10 stored fields, through:
Each stage contains five synchronous virtuals and one synchronous property resolver. The workload includes the memory adapter, hook dispatch, resolver integration, and external dispatch generation.
At concurrency 1, average elapsed time per call decreased from approximately 1.089 ms to 0.710 ms.
The concurrency-8 results measure aggregate throughput. They do not represent individual request latency, and no latency percentiles were collected.
These workloads include no HTTP transport, database, or network costs. The results demonstrate resolver and in-process service improvements; they do not establish the same percentage improvement for applications dominated by external I/O.
Closes #3492