@@ -62,7 +62,8 @@ type Resolvable struct {
6262
6363 enclosingTypeNames []string
6464
65- currentFieldInfo * FieldInfo
65+ currentFieldInfo * FieldInfo
66+ incrementalItemWritten bool
6667}
6768
6869type ResolvableOptions struct {
@@ -108,6 +109,10 @@ func (r *Resolvable) Reset() {
108109 for k := range r .authorizationDeny {
109110 delete (r .authorizationDeny , k )
110111 }
112+ r .deferMode = false
113+ r .deferID = ""
114+ r .enableDeferRender = false
115+ r .incrementalItemWritten = false
111116}
112117
113118func (r * Resolvable ) Init (ctx * Context , initialData []byte , operationType ast.OperationType ) (err error ) {
@@ -189,6 +194,27 @@ func (r *Resolvable) ResolveNode(node Node, data *astjson.Value, out io.Writer)
189194 return nil
190195}
191196
197+ func (r * Resolvable ) renderPath () {
198+ r .printBytes (lBrack )
199+ for i , p := range r .path {
200+ if i > 0 {
201+ r .printBytes (comma )
202+ }
203+ if p .Name != "" {
204+ r .printBytes (quote )
205+ r .printBytes (unsafebytes .StringToBytes (p .Name ))
206+ r .printBytes (quote )
207+ } else {
208+ r .printBytes (unsafebytes .StringToBytes (strconv .Itoa (p .Idx )))
209+ }
210+ }
211+ r .printBytes (rBrack )
212+ }
213+
214+ func (r * Resolvable ) printDeferDelimeter () {
215+ r .printBytes (literalNewLine )
216+ }
217+
192218func (r * Resolvable ) Resolve (ctx context.Context , rootData * Object , fetchTree * FetchTreeNode , out io.Writer ) error {
193219 r .out = out
194220 r .enableRender = false
@@ -236,7 +262,70 @@ func (r *Resolvable) Resolve(ctx context.Context, rootData *Object, fetchTree *F
236262 r .printBytes (comma )
237263 r .printErr = r .printExtensions (ctx , fetchTree )
238264 }
265+
266+ if r .deferMode {
267+ r .printHasNext (true )
268+ }
269+
239270 r .printBytes (rBrace )
271+
272+ if r .deferMode {
273+ r .printDeferDelimeter ()
274+ }
275+
276+ return r .printErr
277+ }
278+
279+ func (r * Resolvable ) ResolveDefer (rootData * Object , out io.Writer , hasNext bool ) error {
280+ r .out = out
281+ r .printErr = nil
282+ r .authorizationError = nil
283+
284+ // This method acts as a generator for the incremental response
285+ // It will print the incremental response envelope and then use walkObject to find and render the deferred fields
286+
287+ // First pass: validate and check for authorization errors
288+ r .enableRender = false
289+ r .deferMode = true
290+ r .enableDeferRender = false
291+
292+ _ = r .walkObject (rootData , r .data )
293+ if r .authorizationError != nil {
294+ return r .authorizationError
295+ }
296+
297+ // Second pass: render the incremental response
298+ r .enableRender = true
299+ r .incrementalItemWritten = false
300+ // deferMode stays true
301+ // enableDeferRender starts false, will be toggled in walkObject when match found
302+
303+ r .printBytes (lBrace )
304+ r .printBytes (quote )
305+ r .printBytes (literalIncremental )
306+ r .printBytes (quote )
307+ r .printBytes (colon )
308+ r .printBytes (lBrack )
309+
310+ _ = r .walkObject (rootData , r .data )
311+
312+ r .printBytes (rBrack )
313+
314+ r .printHasNext (hasNext )
315+
316+ if r .hasErrors () {
317+ r .printBytes (comma )
318+ r .printBytes (quote )
319+ r .printBytes (literalErrors )
320+ r .printBytes (quote )
321+ r .printBytes (colon )
322+ r .printNode (r .errors )
323+ }
324+
325+ r .printBytes (rBrace )
326+
327+ r .printDeferDelimeter ()
328+
240329 return r .printErr
241330}
242331
@@ -645,6 +734,79 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
645734 defer func () {
646735 r .typeNames = r .typeNames [:len (r .typeNames )- 1 ]
647736 }()
737+
738+ // In Defer Seeking Mode, we first identify and render all matching fields for the current DeferID as a single incremental item.
739+ if r .deferMode && ! r .enableDeferRender {
740+ var (
741+ deferFieldIndices []int
742+ )
743+
744+ for k := range obj .Fields {
745+ if obj .Fields [k ].Defer == nil || obj .Fields [k ].Defer .DeferID != r .deferID {
746+ continue
747+ }
748+
749+ // Duplicate skip checks to ensure we only include valid fields
750+ if obj .Fields [k ].ParentOnTypeNames != nil {
751+ if r .skipFieldOnParentTypeNames (obj .Fields [k ]) {
752+ continue
753+ }
754+ }
755+ if obj .Fields [k ].OnTypeNames != nil {
756+ if r .skipFieldOnTypeNames (obj .Fields [k ]) {
757+ continue
758+ }
759+ }
760+
761+ deferFieldIndices = append (deferFieldIndices , k )
762+ }
763+
764+ if len (deferFieldIndices ) > 0 && r .enableRender {
765+ if r .incrementalItemWritten {
766+ r .printBytes (comma )
767+ }
768+
769+ // Render Incremental Item Envelope: {"data":{...},"path":[...]}
770+ r .printBytes (lBrace )
771+
772+ r .printBytes (quote )
773+ r .printBytes (literalData )
774+ r .printBytes (quote )
775+ r .printBytes (colon )
776+ r .printBytes (lBrace )
777+
778+ for k , fieldIdx := range deferFieldIndices {
779+ if k > 0 {
780+ r .printBytes (comma )
781+ }
782+
783+ r .enableDeferRender = true
784+ r .printBytes (quote )
785+ r .printBytes (obj .Fields [fieldIdx ].Name )
786+ r .printBytes (quote )
787+ r .printBytes (colon )
788+
789+ r .currentFieldInfo = obj .Fields [fieldIdx ].Info
790+ _ = r .walkNode (obj .Fields [fieldIdx ].Value , value )
791+ r .enableDeferRender = false
792+ }
793+
794+ r .printBytes (rBrace )
795+
796+ r .printBytes (comma )
797+ r .printBytes (quote )
798+ r .printBytes (literalPath )
799+ r .printBytes (quote )
800+ r .printBytes (colon )
801+ r .renderPath ()
802+
803+ r .printBytes (rBrace )
804+
805+ r .wroteData = true
806+ r .incrementalItemWritten = true
807+ }
808+ }
809+
648810 for i := range obj .Fields {
649811 if obj .Fields [i ].ParentOnTypeNames != nil {
650812 if r .skipFieldOnParentTypeNames (obj .Fields [i ]) {
@@ -656,6 +818,38 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
656818 continue
657819 }
658820 }
821+
822+ // When NOT in defer mode (initial response), skip fields that are deferred.
823+ // They will be handled by the deferred response.
824+ // Also if in deferMode but deferID is empty, it means we are in the initial response of a deferred request.
825+ if obj .Fields [i ].Defer != nil {
826+ if ! r .deferMode || (r .deferMode && r .deferID == "" ) {
827+ continue
828+ }
829+ }
830+
831+ if r .deferMode && ! r .enableDeferRender {
832+ // DEFER SEEKING MODE
833+
834+ // Check if this field matches the current defer ID
835+ isMatch := obj .Fields [i ].Defer != nil && obj .Fields [i ].Defer .DeferID == r .deferID
836+
837+ if isMatch {
838+ // Match found - already rendered in pre-scan
839+ continue
840+ }
841+
842+ // No match - recurse to find nested defers
843+ // We only need to recurse if the node is an Object or Array, as Scalars cannot have nested defers.
844+ // Recursing into Scalars would trigger "non-nullable field returned null" error in handleNodeNotRendered because we are not rendering them.
845+ kind := obj .Fields [i ].Value .NodeKind ()
846+ if kind == NodeKindObject || kind == NodeKindArray {
847+ r .currentFieldInfo = obj .Fields [i ].Info
848+ _ = r .walkNode (obj .Fields [i ].Value , value )
849+ }
850+ continue
851+ }
852+
659853 if ! r .render () {
660854 skip := r .authorizeField (value , obj .Fields [i ])
661855 if skip {
0 commit comments