@@ -60,7 +60,8 @@ type Resolvable struct {
6060
6161 enclosingTypeNames []string
6262
63- currentFieldInfo * FieldInfo
63+ currentFieldInfo * FieldInfo
64+ incrementalItemWritten bool
6465}
6566
6667type ResolvableOptions struct {
@@ -106,6 +107,10 @@ func (r *Resolvable) Reset() {
106107 for k := range r .authorizationDeny {
107108 delete (r .authorizationDeny , k )
108109 }
110+ r .deferMode = false
111+ r .deferID = ""
112+ r .enableDeferRender = false
113+ r .incrementalItemWritten = false
109114}
110115
111116func (r * Resolvable ) Init (ctx * Context , initialData []byte , operationType ast.OperationType ) (err error ) {
@@ -186,6 +191,27 @@ func (r *Resolvable) ResolveNode(node Node, data *astjson.Value, out io.Writer)
186191 return nil
187192}
188193
194+ func (r * Resolvable ) renderPath () {
195+ r .printBytes (lBrack )
196+ for i , p := range r .path {
197+ if i > 0 {
198+ r .printBytes (comma )
199+ }
200+ if p .Name != "" {
201+ r .printBytes (quote )
202+ r .printBytes (unsafebytes .StringToBytes (p .Name ))
203+ r .printBytes (quote )
204+ } else {
205+ r .printBytes (unsafebytes .StringToBytes (strconv .Itoa (p .Idx )))
206+ }
207+ }
208+ r .printBytes (rBrack )
209+ }
210+
211+ func (r * Resolvable ) printDeferDelimeter () {
212+ r .printBytes (literalNewLine )
213+ }
214+
189215func (r * Resolvable ) Resolve (ctx context.Context , rootData * Object , fetchTree * FetchTreeNode , out io.Writer ) error {
190216 r .out = out
191217 r .enableRender = false
@@ -233,7 +259,70 @@ func (r *Resolvable) Resolve(ctx context.Context, rootData *Object, fetchTree *F
233259 r .printBytes (comma )
234260 r .printErr = r .printExtensions (ctx , fetchTree )
235261 }
262+
263+ if r .deferMode {
264+ r .printHasNext (true )
265+ }
266+
236267 r .printBytes (rBrace )
268+
269+ if r .deferMode {
270+ r .printDeferDelimeter ()
271+ }
272+
273+ return r .printErr
274+ }
275+
276+ func (r * Resolvable ) ResolveDefer (rootData * Object , out io.Writer , hasNext bool ) error {
277+ r .out = out
278+ r .printErr = nil
279+ r .authorizationError = nil
280+
281+ // This method acts as a generator for the incremental response
282+ // It will print the incremental response envelope and then use walkObject to find and render the deferred fields
283+
284+ // First pass: validate and check for authorization errors
285+ r .enableRender = false
286+ r .deferMode = true
287+ r .enableDeferRender = false
288+
289+ _ = r .walkObject (rootData , r .data )
290+ if r .authorizationError != nil {
291+ return r .authorizationError
292+ }
293+
294+ // Second pass: render the incremental response
295+ r .enableRender = true
296+ r .incrementalItemWritten = false
297+ // deferMode stays true
298+ // enableDeferRender starts false, will be toggled in walkObject when match found
299+
300+ r .printBytes (lBrace )
301+ r .printBytes (quote )
302+ r .printBytes (literalIncremental )
303+ r .printBytes (quote )
304+ r .printBytes (colon )
305+ r .printBytes (lBrack )
306+
307+ _ = r .walkObject (rootData , r .data )
308+
309+ r .printBytes (rBrack )
310+
311+ r .printHasNext (hasNext )
312+
313+ if r .hasErrors () {
314+ r .printBytes (comma )
315+ r .printBytes (quote )
316+ r .printBytes (literalErrors )
317+ r .printBytes (quote )
318+ r .printBytes (colon )
319+ r .printNode (r .errors )
320+ }
321+
322+ r .printBytes (rBrace )
323+
324+ r .printDeferDelimeter ()
325+
237326 return r .printErr
238327}
239328
@@ -635,6 +724,79 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
635724 defer func () {
636725 r .typeNames = r .typeNames [:len (r .typeNames )- 1 ]
637726 }()
727+
728+ // In Defer Seeking Mode, we first identify and render all matching fields for the current DeferID as a single incremental item.
729+ if r .deferMode && ! r .enableDeferRender {
730+ var (
731+ deferFieldIndices []int
732+ )
733+
734+ for k := range obj .Fields {
735+ if obj .Fields [k ].Defer == nil || obj .Fields [k ].Defer .DeferID != r .deferID {
736+ continue
737+ }
738+
739+ // Duplicate skip checks to ensure we only include valid fields
740+ if obj .Fields [k ].ParentOnTypeNames != nil {
741+ if r .skipFieldOnParentTypeNames (obj .Fields [k ]) {
742+ continue
743+ }
744+ }
745+ if obj .Fields [k ].OnTypeNames != nil {
746+ if r .skipFieldOnTypeNames (obj .Fields [k ]) {
747+ continue
748+ }
749+ }
750+
751+ deferFieldIndices = append (deferFieldIndices , k )
752+ }
753+
754+ if len (deferFieldIndices ) > 0 && r .enableRender {
755+ if r .incrementalItemWritten {
756+ r .printBytes (comma )
757+ }
758+
759+ // Render Incremental Item Envelope: {"data":{...},"path":[...]}
760+ r .printBytes (lBrace )
761+
762+ r .printBytes (quote )
763+ r .printBytes (literalData )
764+ r .printBytes (quote )
765+ r .printBytes (colon )
766+ r .printBytes (lBrace )
767+
768+ for k , fieldIdx := range deferFieldIndices {
769+ if k > 0 {
770+ r .printBytes (comma )
771+ }
772+
773+ r .enableDeferRender = true
774+ r .printBytes (quote )
775+ r .printBytes (obj .Fields [fieldIdx ].Name )
776+ r .printBytes (quote )
777+ r .printBytes (colon )
778+
779+ r .currentFieldInfo = obj .Fields [fieldIdx ].Info
780+ _ = r .walkNode (obj .Fields [fieldIdx ].Value , value )
781+ r .enableDeferRender = false
782+ }
783+
784+ r .printBytes (rBrace )
785+
786+ r .printBytes (comma )
787+ r .printBytes (quote )
788+ r .printBytes (literalPath )
789+ r .printBytes (quote )
790+ r .printBytes (colon )
791+ r .renderPath ()
792+
793+ r .printBytes (rBrace )
794+
795+ r .wroteData = true
796+ r .incrementalItemWritten = true
797+ }
798+ }
799+
638800 for i := range obj .Fields {
639801 if obj .Fields [i ].ParentOnTypeNames != nil {
640802 if r .skipFieldOnParentTypeNames (obj .Fields [i ]) {
@@ -646,6 +808,38 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
646808 continue
647809 }
648810 }
811+
812+ // When NOT in defer mode (initial response), skip fields that are deferred.
813+ // They will be handled by the deferred response.
814+ // Also if in deferMode but deferID is empty, it means we are in the initial response of a deferred request.
815+ if obj .Fields [i ].Defer != nil {
816+ if ! r .deferMode || (r .deferMode && r .deferID == "" ) {
817+ continue
818+ }
819+ }
820+
821+ if r .deferMode && ! r .enableDeferRender {
822+ // DEFER SEEKING MODE
823+
824+ // Check if this field matches the current defer ID
825+ isMatch := obj .Fields [i ].Defer != nil && obj .Fields [i ].Defer .DeferID == r .deferID
826+
827+ if isMatch {
828+ // Match found - already rendered in pre-scan
829+ continue
830+ }
831+
832+ // No match - recurse to find nested defers
833+ // We only need to recurse if the node is an Object or Array, as Scalars cannot have nested defers.
834+ // Recursing into Scalars would trigger "non-nullable field returned null" error in handleNodeNotRendered because we are not rendering them.
835+ kind := obj .Fields [i ].Value .NodeKind ()
836+ if kind == NodeKindObject || kind == NodeKindArray {
837+ r .currentFieldInfo = obj .Fields [i ].Info
838+ _ = r .walkNode (obj .Fields [i ].Value , value )
839+ }
840+ continue
841+ }
842+
649843 if ! r .render () {
650844 skip := r .authorizeField (value , obj .Fields [i ])
651845 if skip {
0 commit comments