From b370b08058312e96432ae37205e9ecc54eaf0b57 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Mon, 22 Jun 2026 09:20:20 +0000 Subject: [PATCH 1/4] early execution and merge --- .../src/__tests__/executeWithSchema.test.ts | 730 ++++++++++++++++++ .../supermassive/src/executeWithSchema.ts | 4 + .../supermassive/src/executeWithoutSchema.ts | 146 +++- .../src/jsutils/getObjectAtPath.ts | 22 + packages/supermassive/src/types.ts | 2 + 5 files changed, 890 insertions(+), 14 deletions(-) create mode 100644 packages/supermassive/src/__tests__/executeWithSchema.test.ts create mode 100644 packages/supermassive/src/jsutils/getObjectAtPath.ts diff --git a/packages/supermassive/src/__tests__/executeWithSchema.test.ts b/packages/supermassive/src/__tests__/executeWithSchema.test.ts new file mode 100644 index 000000000..7a2c3b778 --- /dev/null +++ b/packages/supermassive/src/__tests__/executeWithSchema.test.ts @@ -0,0 +1,730 @@ +import { parse } from "graphql"; +import { executeWithSchema } from "../executeWithSchema"; + +describe("executeWithSchema - @defer behavior", () => { + function createDeferred() { + let resolve: (value: T) => void; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return { promise, resolve: resolve! }; + } + + const definitions = parse(` + type Query { obj: Obj } + type Obj { + critical: String + deferred: String + } + `); + + const document = parse(` + { + obj { + critical + ... on Obj @defer { + deferred + } + } + } + `); + + const nestedDefinitions = parse(` + type Query { obj: Obj } + type Obj { + critical: String + nested: Nested + } + type Nested { + deferred: String + } + `); + + const nestedDocument = parse(` + { + obj { + critical + ... on Obj @defer { + nested { + deferred + } + } + } + } + `); + + const mutationDefinitions = parse(` + type Query { noop: String } + type Mutation { + critical: String + deferred: String + } + `); + + const mutationDocument = parse(` + mutation { + critical + ... on Mutation @defer { + deferred + } + } + `); + + type Resolver = () => string | Promise; + + function executeTestQuery(Obj: { critical: Resolver; deferred: Resolver }) { + return Promise.resolve( + executeWithSchema({ + document, + definitions, + enableEarlyExecution: true, + enableDeferredMerge: true, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj, + }, + }), + ); + } + + function executeNestedTestQuery(Obj: { + critical: Resolver; + deferred: Resolver; + }) { + return Promise.resolve( + executeWithSchema({ + document: nestedDocument, + definitions: nestedDefinitions, + enableEarlyExecution: true, + enableDeferredMerge: true, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj: { + critical: Obj.critical, + nested: () => ({}), + }, + Nested: { + deferred: Obj.deferred, + }, + }, + }), + ); + } + + function executeTestMutation(Mutation: { + critical: Resolver; + deferred: Resolver; + }) { + return Promise.resolve( + executeWithSchema({ + document: mutationDocument, + definitions: mutationDefinitions, + enableEarlyExecution: true, + enableDeferredMerge: true, + resolvers: { + Mutation, + }, + }), + ); + } + + test("returns the initial response as soon as critical fields are ready", async () => { + const critical = createDeferred(); + const deferred = createDeferred(); + + const resultPromise = executeTestQuery({ + critical: () => critical.promise, + deferred: () => deferred.promise, + }); + + critical.resolve("critical"); + const result = await Promise.race([ + resultPromise, + new Promise<"blocked">((resolve) => setTimeout(resolve, 0, "blocked")), + ]); + + if (result === "blocked") { + throw new Error("Initial response waited for deferred field"); + } + + expect(result).toMatchObject({ + initialResult: { + data: { + obj: { + critical: "critical", + }, + }, + hasNext: true, + }, + }); + + if (!("initialResult" in result)) { + throw new Error("Expected an incremental result"); + } + + const subsequentResultPromise = result.subsequentResults.next(); + deferred.resolve("deferred"); + + await expect(subsequentResultPromise).resolves.toMatchObject({ + value: { + incremental: [ + { + data: { + deferred: "deferred", + }, + path: ["obj"], + }, + ], + hasNext: false, + }, + done: false, + }); + }); + + test("includes deferred fields in the initial response when they complete before the critical fields", async () => { + const critical = createDeferred(); + + const resultPromise = executeTestQuery({ + deferred: () => Promise.resolve("deferred"), + critical: () => critical.promise, + }); + + setTimeout(() => critical.resolve("critical"), 0); + + await expect(resultPromise).resolves.toEqual({ + data: { + obj: { + critical: "critical", + deferred: "deferred", + }, + }, + }); + }); + + test("surfaces deferred errors at the top level when piggybacked onto the initial response", async () => { + const critical = createDeferred(); + + const resultPromise = executeTestQuery({ + deferred: () => { + throw new Error("Deferred boom"); + }, + critical: () => critical.promise, + }); + + setTimeout(() => critical.resolve("critical"), 0); + + const result = await resultPromise; + + expect(result).toMatchObject({ + data: { + obj: { + critical: "critical", + deferred: null, + }, + }, + }); + expect("initialResult" in (result as object)).toBe(false); + expect( + (result as { errors?: ReadonlyArray<{ message: string }> }).errors, + ).toHaveLength(1); + expect( + (result as { errors: ReadonlyArray<{ message: string }> }).errors[0] + .message, + ).toBe("Deferred boom"); + }); + + test("folds a synchronous deferred field into the initial response instead of streaming it", async () => { + const result = await executeTestQuery({ + critical: () => "critical", + deferred: () => "deferred", + }); + + // Both fields resolve synchronously, so the deferred fragment is already + // complete by the time the response is built. The optimization folds it + // into the initial data rather than emitting a redundant incremental + // payload, producing a single (non-incremental) result. + expect(result).toEqual({ + data: { + obj: { + critical: "critical", + deferred: "deferred", + }, + }, + }); + expect("initialResult" in (result as object)).toBe(false); + }); + + test("delivers a synchronously-completed deferred field incrementally when merge is disabled", async () => { + // Early execution starts the deferred fragment eagerly, but with merge + // disabled it is never folded into the initial response (mirroring + // graphql-js's `enableEarlyExecution`): it is always delivered as a + // separate incremental payload. + const result = await executeWithSchema({ + document, + definitions, + enableEarlyExecution: true, + enableDeferredMerge: false, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj: { + critical: () => "critical", + deferred: () => "deferred", + }, + }, + }); + + expect(result).toMatchObject({ + initialResult: { + data: { + obj: { + critical: "critical", + }, + }, + hasNext: true, + }, + }); + + if (!("initialResult" in result)) { + throw new Error("Expected an incremental result"); + } + + expect(result.initialResult.data).toEqual({ + obj: { critical: "critical" }, + }); + + await expect(result.subsequentResults.next()).resolves.toMatchObject({ + value: { + incremental: [ + { + data: { + deferred: "deferred", + }, + path: ["obj"], + }, + ], + hasNext: false, + }, + done: false, + }); + }); + + test("keeps legacy defer behavior when early execution is disabled", async () => { + const result = await executeWithSchema({ + document, + definitions, + enableEarlyExecution: false, + enableDeferredMerge: false, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj: { + critical: () => "critical", + deferred: () => "deferred", + }, + }, + }); + + expect(result).toMatchObject({ + initialResult: { + data: { + obj: { + critical: "critical", + }, + }, + hasNext: true, + }, + }); + + if (!("initialResult" in result)) { + throw new Error("Expected an incremental result"); + } + + await expect(result.subsequentResults.next()).resolves.toMatchObject({ + value: { + incremental: [ + { + data: { + deferred: "deferred", + }, + path: ["obj"], + }, + ], + hasNext: false, + }, + done: false, + }); + }); + + test("includes nested deferred field in the initial response when it settles before critical", async () => { + const critical = createDeferred(); + + const resultPromise = executeNestedTestQuery({ + deferred: () => Promise.resolve("deferred"), + critical: () => critical.promise, + }); + + setTimeout(() => critical.resolve("critical"), 0); + + await expect(resultPromise).resolves.toEqual({ + data: { + obj: { + critical: "critical", + nested: { + deferred: "deferred", + }, + }, + }, + }); + }); + + test("streams nested deferred field when critical fields are ready first", async () => { + const deferred = createDeferred(); + + const result = await executeNestedTestQuery({ + critical: () => "critical", + deferred: () => deferred.promise, + }); + + expect(result).toMatchObject({ + initialResult: { + data: { + obj: { + critical: "critical", + }, + }, + hasNext: true, + }, + }); + + if (!("initialResult" in result)) { + throw new Error("Expected an incremental result"); + } + + const subsequentResultPromise = result.subsequentResults.next(); + deferred.resolve("deferred"); + + await expect(subsequentResultPromise).resolves.toMatchObject({ + value: { + incremental: [ + { + data: { + nested: { + deferred: "deferred", + }, + }, + path: ["obj"], + }, + ], + hasNext: false, + }, + done: false, + }); + }); + + test("includes deferred mutation fields in the initial response when they settle before critical", async () => { + const critical = createDeferred(); + + const resultPromise = executeTestMutation({ + critical: () => critical.promise, + deferred: () => Promise.resolve("deferred"), + }); + + setTimeout(() => critical.resolve("critical"), 0); + + await expect(resultPromise).resolves.toEqual({ + data: { + critical: "critical", + deferred: "deferred", + }, + }); + }); + + const twoDeferDefinitions = parse(` + type Query { obj: Obj } + type Obj { + critical: String + deferredEarly: String + deferredLate: String + } + `); + + const twoDeferDocument = parse(` + { + obj { + critical + ... on Obj @defer(label: "early") { + deferredEarly + } + ... on Obj @defer(label: "late") { + deferredLate + } + } + } + `); + + function executeTwoDeferQuery(Obj: { + critical: Resolver; + deferredEarly: Resolver; + deferredLate: Resolver; + }) { + return Promise.resolve( + executeWithSchema({ + document: twoDeferDocument, + definitions: twoDeferDefinitions, + enableEarlyExecution: true, + enableDeferredMerge: true, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj, + }, + }), + ); + } + + test("folds the deferred fragment that settled before critical and streams the one that settled after", async () => { + const critical = createDeferred(); + const late = createDeferred(); + + const resultPromise = executeTwoDeferQuery({ + // Resolves synchronously, so this fragment completes before critical. + deferredEarly: () => "early", + // Stays pending past the initial response, so this fragment streams. + deferredLate: () => late.promise, + critical: () => critical.promise, + }); + + setTimeout(() => critical.resolve("critical"), 0); + + const result = await resultPromise; + + // The early fragment is folded into the initial response; the late one is + // still pending, so the overall result is incremental. + expect(result).toMatchObject({ + initialResult: { + data: { + obj: { + critical: "critical", + deferredEarly: "early", + }, + }, + hasNext: true, + }, + }); + + if (!("initialResult" in result)) { + throw new Error("Expected an incremental result"); + } + + expect(result.initialResult.data).toEqual({ + obj: { critical: "critical", deferredEarly: "early" }, + }); + + const subsequentResultPromise = result.subsequentResults.next(); + late.resolve("late"); + + await expect(subsequentResultPromise).resolves.toMatchObject({ + value: { + incremental: [ + { + data: { + deferredLate: "late", + }, + path: ["obj"], + }, + ], + hasNext: false, + }, + done: false, + }); + }); + + const ifDocument = parse(` + query ($shouldDefer: Boolean!) { + obj { + critical + ... on Obj @defer(if: $shouldDefer) { + deferred + } + } + } + `); + + function executeIfTestQuery( + Obj: { critical: Resolver; deferred: Resolver }, + shouldDefer: boolean, + ) { + return Promise.resolve( + executeWithSchema({ + document: ifDocument, + definitions, + enableEarlyExecution: true, + enableDeferredMerge: true, + variableValues: { shouldDefer }, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj, + }, + }), + ); + } + + test("streams the deferred field when @defer(if: true) and critical is ready first", async () => { + const deferred = createDeferred(); + + const result = await executeIfTestQuery( + { + critical: () => "critical", + deferred: () => deferred.promise, + }, + true, + ); + + expect(result).toMatchObject({ + initialResult: { + data: { + obj: { + critical: "critical", + }, + }, + hasNext: true, + }, + }); + + if (!("initialResult" in result)) { + throw new Error("Expected an incremental result"); + } + + const subsequentResultPromise = result.subsequentResults.next(); + deferred.resolve("deferred"); + + await expect(subsequentResultPromise).resolves.toMatchObject({ + value: { + incremental: [ + { + data: { + deferred: "deferred", + }, + path: ["obj"], + }, + ], + hasNext: false, + }, + done: false, + }); + }); + + test("does not defer the field when @defer(if: false), even if it is slow", async () => { + const deferred = createDeferred(); + + const resultPromise = executeIfTestQuery( + { + critical: () => "critical", + deferred: () => deferred.promise, + }, + false, + ); + + // With `if: false` the fragment is not deferred, so the field is critical + // and the response must wait for it instead of streaming it. + const raced = await Promise.race([ + resultPromise, + new Promise<"pending">((resolve) => setTimeout(resolve, 0, "pending")), + ]); + + expect(raced).toBe("pending"); + + deferred.resolve("deferred"); + + await expect(resultPromise).resolves.toEqual({ + data: { + obj: { + critical: "critical", + deferred: "deferred", + }, + }, + }); + }); + + test("returns a single non-incremental result when @defer(if: false)", async () => { + const result = await executeIfTestQuery( + { + critical: () => "critical", + deferred: () => "deferred", + }, + false, + ); + + expect(result).toEqual({ + data: { + obj: { + critical: "critical", + deferred: "deferred", + }, + }, + }); + expect("initialResult" in (result as object)).toBe(false); + }); + + // Schema where the critical field is non-null: when it errors, the null + // bubbles up and destroys the whole `obj` branch the deferred fragment is + // anchored to. + const nonNullCriticalDefinitions = parse(` + type Query { obj: Obj } + type Obj { + critical: String! + deferred: String + } + `); + + function executeNonNullCriticalQuery(Obj: { + critical: Resolver; + deferred: Resolver; + }) { + return Promise.resolve( + executeWithSchema({ + document, + definitions: nonNullCriticalDefinitions, + resolvers: { + Query: { + obj: () => ({}), + }, + Obj, + }, + }), + ); + } + + test("does not fold a synchronously-completed deferred fragment when a non-null sibling error nulls its parent", async () => { + const result = await executeNonNullCriticalQuery({ + critical: () => { + throw new Error("Critical boom"); + }, + deferred: () => "deferred", + }); + + // The non-null `critical` error bubbles up and nulls `obj`. The deferred + // fragment completed synchronously, but its anchor object is gone, so it + // must not be folded into the (now null) branch. + expect(result).toMatchObject({ + data: { + obj: null, + }, + }); + expect("initialResult" in (result as object)).toBe(false); + const errors = (result as { errors?: ReadonlyArray<{ message: string }> }) + .errors; + expect(errors).toHaveLength(1); + expect(errors?.[0].message).toBe("Critical boom"); + }); +}); diff --git a/packages/supermassive/src/executeWithSchema.ts b/packages/supermassive/src/executeWithSchema.ts index 2b5d242a8..eed97b305 100644 --- a/packages/supermassive/src/executeWithSchema.ts +++ b/packages/supermassive/src/executeWithSchema.ts @@ -16,6 +16,8 @@ export function executeWithSchema({ typeResolver, fieldExecutionHooks, enablePerEventContext, + enableEarlyExecution, + enableDeferredMerge, }: ExecutionWithSchemaArgs): PromiseOrValue { const extracted = extractMinimalViableSchemaForRequestDocument( buildASTSchema(definitions), @@ -36,5 +38,7 @@ export function executeWithSchema({ typeResolver, fieldExecutionHooks, enablePerEventContext, + enableEarlyExecution, + enableDeferredMerge, }); } diff --git a/packages/supermassive/src/executeWithoutSchema.ts b/packages/supermassive/src/executeWithoutSchema.ts index 340c4523c..4d613348a 100644 --- a/packages/supermassive/src/executeWithoutSchema.ts +++ b/packages/supermassive/src/executeWithoutSchema.ts @@ -13,8 +13,10 @@ import { collectSubfields as _collectSubfields, FieldGroup, GroupedFieldSet, + PatchFields, } from "./collectFields"; import { devAssert } from "./jsutils/devAssert"; +import { getObjectAtPath } from "./jsutils/getObjectAtPath"; import { inspect } from "./jsutils/inspect"; import { invariant } from "./jsutils/invariant"; import { isIterableObject } from "./jsutils/isIterableObject"; @@ -122,6 +124,8 @@ export interface ExecutionContext { fieldExecutionHooks?: ExecutionHooks; subsequentPayloads: Set; enablePerEventContext: boolean; + enableEarlyExecution: boolean; + enableDeferredMerge: boolean; } /** @@ -193,6 +197,8 @@ function buildExecutionContext( subscribeFieldResolver, fieldExecutionHooks, enablePerEventContext, + enableEarlyExecution, + enableDeferredMerge, } = args; assertValidExecutionArguments(document, variableValues); @@ -262,6 +268,8 @@ function buildExecutionContext( fieldExecutionHooks, subsequentPayloads: new Set(), enablePerEventContext: enablePerEventContext ?? true, + enableEarlyExecution: enableEarlyExecution ?? false, + enableDeferredMerge: enableDeferredMerge ?? false, }; } @@ -328,6 +336,7 @@ function executeOperation( path, groupedFieldSet, undefined, + exeContext.enableEarlyExecution ? patches : undefined, ); result = buildResponse(exeContext, result); break; @@ -338,6 +347,7 @@ function executeOperation( rootValue, path, groupedFieldSet, + exeContext.enableEarlyExecution ? patches : undefined, ); result = buildResponse(exeContext, result); break; @@ -359,16 +369,18 @@ function executeOperation( ); } - for (const patch of patches) { - const { label, groupedFieldSet: patchGroupedFieldSet } = patch; - executeDeferredFragment( - exeContext, - rootTypeName, - rootValue, - patchGroupedFieldSet, - label, - path, - ); + if (!exeContext.enableEarlyExecution) { + for (const patch of patches) { + const { label, groupedFieldSet: patchGroupedFieldSet } = patch; + executeDeferredFragment( + exeContext, + rootTypeName, + rootValue, + patchGroupedFieldSet, + label, + path, + ); + } } return result; @@ -398,6 +410,10 @@ function buildResponse( const hooks = exeContext.fieldExecutionHooks; try { + if (exeContext.enableDeferredMerge) { + includeCompletedDeferredFragmentsInResult(exeContext, data); + } + const initialResult = exeContext.errors.length === 0 ? { data } @@ -442,7 +458,18 @@ function executeFieldsSerially( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, + patches?: Array, ): PromiseOrValue> { + if (exeContext.enableEarlyExecution) { + startExecutingPatches( + exeContext, + parentTypeName, + sourceValue, + path, + patches, + undefined, + ); + } return promiseReduce( groupedFieldSet, (results, [responseName, fieldGroup]) => { @@ -486,6 +513,7 @@ function executeFields( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalDataRecord: IncrementalDataRecord | undefined, + patches?: Array, ): PromiseOrValue> { const results = Object.create(null); let containsPromise = false; @@ -519,6 +547,15 @@ function executeFields( throw error; } + startExecutingPatches( + exeContext, + parentTypeName, + sourceValue, + path, + patches, + incrementalDataRecord, + ); + // If there are no promises, we can just return the object if (!containsPromise) { return results; @@ -530,6 +567,64 @@ function executeFields( return promiseForObject(results); } +function startExecutingPatches( + exeContext: ExecutionContext, + parentTypeName: string, + sourceValue: unknown, + path: Path | undefined, + patches: Array | undefined, + incrementalDataRecord: IncrementalDataRecord | undefined, +): void { + if (!patches) { + return; + } + + for (const { label, groupedFieldSet: patchGroupedFieldSet } of patches) { + executeDeferredFragment( + exeContext, + parentTypeName, + sourceValue, + patchGroupedFieldSet, + label, + path, + incrementalDataRecord, + ); + } +} + +function includeCompletedDeferredFragmentsInResult( + exeContext: ExecutionContext, + result: ObjMap | null, +): void { + if (result == null) { + return; + } + + for (const incrementalDataRecord of exeContext.subsequentPayloads) { + if ( + !isDeferredFragmentRecord(incrementalDataRecord) || + !incrementalDataRecord.isCompleted + ) { + continue; + } + + const target = getObjectAtPath(result, incrementalDataRecord.path); + if (!target) { + continue; + } + + if (incrementalDataRecord.data != null) { + Object.assign(target, incrementalDataRecord.data); + } + + if (incrementalDataRecord.errors.length > 0) { + exeContext.errors.push(...incrementalDataRecord.errors); + } + + exeContext.subsequentPayloads.delete(incrementalDataRecord); + } +} + /** * Implements the "Executing field" section of the spec * In particular, this function figures out the value that the field returns by @@ -2003,6 +2098,18 @@ function collectAndExecuteSubfields( const { groupedFieldSet: subGroupedFieldSet, patches: subPatches } = collectSubfields(exeContext, { name: returnTypeName }, fieldGroup); + if (exeContext.enableEarlyExecution) { + return executeFields( + exeContext, + returnTypeName, + result, + path, + subGroupedFieldSet, + incrementalDataRecord, + subPatches, + ); + } + const subFields = executeFields( exeContext, returnTypeName, @@ -2875,6 +2982,12 @@ function isStreamItemsRecord( return incrementalDataRecord.type === "stream"; } +function isDeferredFragmentRecord( + incrementalDataRecord: IncrementalDataRecord, +): incrementalDataRecord is DeferredFragmentRecord { + return incrementalDataRecord.type === "defer"; +} + class DeferredFragmentRecord { type: "defer"; errors: Array; @@ -2906,10 +3019,12 @@ class DeferredFragmentRecord { this._resolve = (promiseOrValue) => { resolve(promiseOrValue); }; - }).then((data) => { - this.data = data; - this.isCompleted = true; - }); + }).then((data) => this.complete(data)); + } + + private complete(data: ObjMap | null): void { + this.data = data; + this.isCompleted = true; } addData(data: PromiseOrValue | null>) { @@ -2918,6 +3033,9 @@ class DeferredFragmentRecord { this._resolve?.(parentData.then(() => data)); return; } + if (!isPromise(data)) { + this.complete(data); + } this._resolve?.(data); } } diff --git a/packages/supermassive/src/jsutils/getObjectAtPath.ts b/packages/supermassive/src/jsutils/getObjectAtPath.ts new file mode 100644 index 000000000..a1b1d4501 --- /dev/null +++ b/packages/supermassive/src/jsutils/getObjectAtPath.ts @@ -0,0 +1,22 @@ +import type { ObjMap } from "./ObjMap"; + +/** + * Walks `result` following `path` and returns the nested value, but only when + * that value is a non-null object (a valid `Object.assign` target). Returns + * `undefined` if any segment is missing or the resolved value is not an object. + */ +export function getObjectAtPath( + result: ObjMap, + path: ReadonlyArray, +): ObjMap | undefined { + let target = result; + for (const key of path) { + const next = target[key]; + if (next === null || typeof next !== "object") { + return undefined; + } + target = next as ObjMap; + } + + return target; +} diff --git a/packages/supermassive/src/types.ts b/packages/supermassive/src/types.ts index 457134685..935181441 100644 --- a/packages/supermassive/src/types.ts +++ b/packages/supermassive/src/types.ts @@ -257,6 +257,8 @@ export interface CommonExecutionArgs { subscribeFieldResolver?: Maybe>; fieldExecutionHooks?: ExecutionHooks; enablePerEventContext?: boolean; + enableEarlyExecution?: boolean; + enableDeferredMerge?: boolean; } export type ExecutionWithoutSchemaArgs = CommonExecutionArgs & { schemaFragment: SchemaFragment; From 281e265576b56a75b77823810e341432143b4134 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Mon, 22 Jun 2026 09:22:13 +0000 Subject: [PATCH 2/4] fixies --- .../supermassive/src/executeWithoutSchema.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/supermassive/src/executeWithoutSchema.ts b/packages/supermassive/src/executeWithoutSchema.ts index 4d613348a..51aacdbb7 100644 --- a/packages/supermassive/src/executeWithoutSchema.ts +++ b/packages/supermassive/src/executeWithoutSchema.ts @@ -518,6 +518,17 @@ function executeFields( const results = Object.create(null); let containsPromise = false; + if (exeContext.enableEarlyExecution) { + startExecutingPatches( + exeContext, + parentTypeName, + sourceValue, + path, + patches, + incrementalDataRecord, + ); + } + try { for (const [responseName, fieldGroup] of groupedFieldSet) { const fieldPath = addPath(path, responseName, parentTypeName); @@ -547,15 +558,6 @@ function executeFields( throw error; } - startExecutingPatches( - exeContext, - parentTypeName, - sourceValue, - path, - patches, - incrementalDataRecord, - ); - // If there are no promises, we can just return the object if (!containsPromise) { return results; From cc14f5be7477ce871cf8967db513a850c0725843 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Mon, 22 Jun 2026 09:22:31 +0000 Subject: [PATCH 3/4] Change files --- ...-supermassive-b119265a-cda6-437b-ad83-e893facef588.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-supermassive-b119265a-cda6-437b-ad83-e893facef588.json diff --git a/change/@graphitation-supermassive-b119265a-cda6-437b-ad83-e893facef588.json b/change/@graphitation-supermassive-b119265a-cda6-437b-ad83-e893facef588.json new file mode 100644 index 000000000..7aa033ac9 --- /dev/null +++ b/change/@graphitation-supermassive-b119265a-cda6-437b-ad83-e893facef588.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "early execution and merge", + "packageName": "@graphitation/supermassive", + "email": "pavelglac@gmail.com", + "dependentChangeType": "patch" +} From cf7dfc9a5d470c1e72efde0988f3d353682182aa Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Mon, 22 Jun 2026 11:51:41 +0000 Subject: [PATCH 4/4] remove the fetch --- .azure-devops/graphitation-release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.azure-devops/graphitation-release.yml b/.azure-devops/graphitation-release.yml index 8d9676411..367a146c9 100644 --- a/.azure-devops/graphitation-release.yml +++ b/.azure-devops/graphitation-release.yml @@ -55,7 +55,6 @@ extends: - script: | git config user.email "gql-svc@microsoft.com" git config user.name "Graphitation Service Account" - git fetch --depth=2 displayName: Configure git for release - script: | releaseBranch="origin/${BUILD_SOURCEBRANCH#refs/heads/}"