From fd3f46b6cb1a8f6b854bc36559366106e69b5231 Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Tue, 26 Nov 2024 15:20:48 -0500 Subject: [PATCH 01/11] update defer tests to use test table --- codegen/testserver/singlefile/defer.graphql | 4 +- codegen/testserver/singlefile/defer_test.go | 376 +++++++++----------- codegen/testserver/singlefile/generated.go | 44 +-- codegen/testserver/singlefile/resolver.go | 8 +- codegen/testserver/singlefile/stub.go | 12 +- 5 files changed, 208 insertions(+), 236 deletions(-) diff --git a/codegen/testserver/singlefile/defer.graphql b/codegen/testserver/singlefile/defer.graphql index c31e0def87a..3d20ea61bcc 100644 --- a/codegen/testserver/singlefile/defer.graphql +++ b/codegen/testserver/singlefile/defer.graphql @@ -1,6 +1,6 @@ extend type Query { - deferCase1: DeferModel - deferCase2: [DeferModel!] + deferSingle: DeferModel + deferMultiple: [DeferModel!] } type DeferModel { diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index d3c75451db0..7576b725801 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -1,9 +1,12 @@ package singlefile import ( + "cmp" "context" "encoding/json" "math/rand" + "reflect" + "slices" "strconv" "strings" "testing" @@ -25,14 +28,14 @@ func TestDefer(t *testing.T) { c := client.New(srv) - resolvers.QueryResolver.DeferCase1 = func(ctx context.Context) (*DeferModel, error) { + resolvers.QueryResolver.DeferSingle = func(ctx context.Context) (*DeferModel, error) { return &DeferModel{ ID: "1", Name: "Defer test 1", }, nil } - resolvers.QueryResolver.DeferCase2 = func(ctx context.Context) ([]*DeferModel, error) { + resolvers.QueryResolver.DeferMultiple = func(ctx context.Context) ([]*DeferModel, error) { return []*DeferModel{ { ID: "1", @@ -58,228 +61,197 @@ func TestDefer(t *testing.T) { }, nil } - t.Run("test deferCase1 using SSE", func(t *testing.T) { - sse := c.SSE(context.Background(), `query testDefer { - deferCase1 { - id - name - ... on DeferModel @defer(label: "values") { - values - } - } -}`) + deferSingleQuery := `query testDefer { + deferSingle { + id + name + ... @defer(label: "values") { + values + } + } + }` + deferMultipleQuery := `query testDefer { + deferMultiple { + id + name + ... @defer(label: "values") { + values + } + } + }` - type response struct { - Data struct { - DeferCase1 struct { - Id string - Name string - Values []string - } - } - Label string `json:"label"` - Path []any `json:"path"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` - } - var resp response - - require.NoError(t, sse.Next(&resp)) - expectedInitialResponse := response{ - Data: struct { - DeferCase1 struct { - Id string - Name string - Values []string - } - }{ - DeferCase1: struct { - Id string - Name string - Values []string - }{ - Id: "1", - Name: "Defer test 1", - Values: nil, - }, - }, - HasNext: true, + type deferModel struct { + Id string + Name string + Values []string + } + type response[T any] struct { + Data T + Label string `json:"label"` + Path []any `json:"path"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` + } + type sseDeferredResponse struct { + Data struct { + Values []string `json:"values"` } - assert.Equal(t, expectedInitialResponse, resp) + Label string `json:"label"` + Path []any `json:"path"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` + } - type valuesResponse struct { - Data struct { - Values []string `json:"values"` + pathStringer := func(path []any) string { + var kb strings.Builder + for i, part := range path { + if i != 0 { + kb.WriteRune('.') } - Label string `json:"label"` - Path []any `json:"path"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` - } - - var valueResp valuesResponse - expectedResponse := valuesResponse{ - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "values", - Path: []any{"deferCase1"}, - } - require.NoError(t, sse.Next(&valueResp)) - - assert.Equal(t, expectedResponse, valueResp) - - require.NoError(t, sse.Close()) - }) - - t.Run("test deferCase2 using SSE", func(t *testing.T) { - sse := c.SSE(context.Background(), `query testDefer { - deferCase2 { - id - name - ... on DeferModel @defer(label: "values") { - values - } - } -}`) - - type response struct { - Data struct { - DeferCase2 []struct { - Id string - Name string - Values []string - } + switch pathValue := part.(type) { + case string: + kb.WriteString(pathValue) + case float64: + kb.WriteString(strconv.FormatFloat(pathValue, 'f', -1, 64)) + default: + t.Fatalf("unexpected path type: %T", pathValue) } - Label string `json:"label"` - Path []any `json:"path"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` } - var resp response + return kb.String() + } - require.NoError(t, sse.Next(&resp)) - expectedInitialResponse := response{ - Data: struct { - DeferCase2 []struct { - Id string - Name string - Values []string - } - }{ - DeferCase2: []struct { - Id string - Name string - Values []string - }{ - { - Id: "1", - Name: "Defer test 1", - Values: nil, - }, - { - Id: "2", - Name: "Defer test 2", - Values: nil, + t.Run("using SSE", func(t *testing.T) { + cases := []struct { + name string + query string + expectedInitialResponse interface{} + expectedDeferredResponses []sseDeferredResponse + }{ + { + name: "defer single", + query: deferSingleQuery, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, }, + HasNext: true, + }, + expectedDeferredResponses: []sseDeferredResponse{ { - Id: "3", - Name: "Defer test 3", - Values: nil, + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "values", + Path: []any{"deferSingle"}, }, }, }, - HasNext: true, - } - assert.Equal(t, expectedInitialResponse, resp) - - type valuesResponse struct { - Data struct { - Values []string `json:"values"` - } - Label string `json:"label"` - Path []any `json:"path"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` - } - - valuesByPath := make(map[string][]string, 2) - - for { - var valueResp valuesResponse - require.NoError(t, sse.Next(&valueResp)) - - var kb strings.Builder - for i, path := range valueResp.Path { - if i != 0 { - kb.WriteRune('.') - } - - switch pathValue := path.(type) { - case string: - kb.WriteString(pathValue) - case float64: - kb.WriteString(strconv.FormatFloat(pathValue, 'f', -1, 64)) - default: - t.Fatalf("unexpected path type: %T", pathValue) - } - } - - valuesByPath[kb.String()] = valueResp.Data.Values - if !valueResp.HasNext { - break - } - } - - assert.Equal(t, []string{"test defer 1", "test defer 2", "test defer 3"}, valuesByPath["deferCase2.0"]) - assert.Equal(t, []string{"test defer 1", "test defer 2", "test defer 3"}, valuesByPath["deferCase2.1"]) - assert.Equal(t, []string{"test defer 1", "test defer 2", "test defer 3"}, valuesByPath["deferCase2.2"]) - - for i := range resp.Data.DeferCase2 { - resp.Data.DeferCase2[i].Values = valuesByPath["deferCase2."+strconv.FormatInt(int64(i), 10)] - } - - expectedDeferCase2Response := response{ - Data: struct { - DeferCase2 []struct { - Id string - Name string - Values []string - } - }{ - DeferCase2: []struct { - Id string - Name string - Values []string - }{ + { + name: "defer multiple", + query: deferMultipleQuery, + expectedInitialResponse: response[struct { + DeferMultiple []deferModel + }]{ + Data: struct { + DeferMultiple []deferModel + }{ + DeferMultiple: []deferModel{ + { + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + { + Id: "2", + Name: "Defer test 2", + Values: nil, + }, + { + Id: "3", + Name: "Defer test 3", + Values: nil, + }, + }, + }, + HasNext: true, + }, + expectedDeferredResponses: []sseDeferredResponse{ { - Id: "1", - Name: "Defer test 1", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "values", + Path: []any{"deferMultiple", float64(0)}, }, { - Id: "2", - Name: "Defer test 2", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "values", + Path: []any{"deferMultiple", float64(1)}, }, { - Id: "3", - Name: "Defer test 3", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "values", + Path: []any{"deferMultiple", float64(2)}, }, }, }, - HasNext: true, } - assert.Equal(t, expectedDeferCase2Response, resp) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + initRespT := reflect.TypeOf(tc.expectedInitialResponse) + + sse := c.SSE(context.Background(), tc.query) + resp := reflect.New(initRespT).Elem().Interface() + require.NoError(t, sse.Next(&resp)) + assert.Equal(t, tc.expectedInitialResponse, resp) + + deferredResponses := make([]sseDeferredResponse, 0) + for { + var valueResp sseDeferredResponse + require.NoError(t, sse.Next(&valueResp)) - require.NoError(t, sse.Close()) + if !valueResp.HasNext { + deferredResponses = append(deferredResponses, valueResp) + break + } else { + // Remove HasNext from comparison: we don't know the order they will be + // delivered in, and so this can't be known in the setup. But if HasNext + // does not work right we will either error out or get too few + // responses, so it's still checked. + valueResp.HasNext = false + deferredResponses = append(deferredResponses, valueResp) + } + } + require.NoError(t, sse.Close()) + + slices.SortFunc(deferredResponses, func(a, b sseDeferredResponse) int { + return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) + }) + assert.Equal(t, tc.expectedDeferredResponses, deferredResponses) + }) + } }) } diff --git a/codegen/testserver/singlefile/generated.go b/codegen/testserver/singlefile/generated.go index ca8bafbae07..bbc45dbf30b 100644 --- a/codegen/testserver/singlefile/generated.go +++ b/codegen/testserver/singlefile/generated.go @@ -335,8 +335,8 @@ type ComplexityRoot struct { Collision func(childComplexity int) int DefaultParameters func(childComplexity int, falsyBoolean *bool, truthyBoolean *bool) int DefaultScalar func(childComplexity int, arg string) int - DeferCase1 func(childComplexity int) int - DeferCase2 func(childComplexity int) int + DeferMultiple func(childComplexity int) int + DeferSingle func(childComplexity int) int DeprecatedField func(childComplexity int) int DirectiveArg func(childComplexity int, arg string) int DirectiveDouble func(childComplexity int) int @@ -553,8 +553,8 @@ type QueryResolver interface { DeprecatedField(ctx context.Context) (string, error) Overlapping(ctx context.Context) (*OverlappingFields, error) DefaultParameters(ctx context.Context, falsyBoolean *bool, truthyBoolean *bool) (*DefaultParametersMirror, error) - DeferCase1(ctx context.Context) (*DeferModel, error) - DeferCase2(ctx context.Context) ([]*DeferModel, error) + DeferSingle(ctx context.Context) (*DeferModel, error) + DeferMultiple(ctx context.Context) ([]*DeferModel, error) DirectiveArg(ctx context.Context, arg string) (*string, error) DirectiveNullableArg(ctx context.Context, arg *int, arg2 *int, arg3 *string) (*string, error) DirectiveSingleNullableArg(ctx context.Context, arg1 *string) (*string, error) @@ -1434,19 +1434,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.DefaultScalar(childComplexity, args["arg"].(string)), true - case "Query.deferCase1": - if e.complexity.Query.DeferCase1 == nil { + case "Query.deferMultiple": + if e.complexity.Query.DeferMultiple == nil { break } - return e.complexity.Query.DeferCase1(childComplexity), true + return e.complexity.Query.DeferMultiple(childComplexity), true - case "Query.deferCase2": - if e.complexity.Query.DeferCase2 == nil { + case "Query.deferSingle": + if e.complexity.Query.DeferSingle == nil { break } - return e.complexity.Query.DeferCase2(childComplexity), true + return e.complexity.Query.DeferSingle(childComplexity), true case "Query.deprecatedField": if e.complexity.Query.DeprecatedField == nil { @@ -10481,8 +10481,8 @@ func (ec *executionContext) fieldContext_Query_defaultParameters(ctx context.Con return fc, nil } -func (ec *executionContext) _Query_deferCase1(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_deferCase1(ctx, field) +func (ec *executionContext) _Query_deferSingle(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_deferSingle(ctx, field) if err != nil { return graphql.Null } @@ -10495,7 +10495,7 @@ func (ec *executionContext) _Query_deferCase1(ctx context.Context, field graphql }() resTmp := ec._fieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DeferCase1(rctx) + return ec.resolvers.Query().DeferSingle(rctx) }) if resTmp == nil { @@ -10506,7 +10506,7 @@ func (ec *executionContext) _Query_deferCase1(ctx context.Context, field graphql return ec.marshalODeferModel2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋcodegenᚋtestserverᚋsinglefileᚐDeferModel(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_deferCase1(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Query_deferSingle(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, @@ -10527,8 +10527,8 @@ func (ec *executionContext) fieldContext_Query_deferCase1(_ context.Context, fie return fc, nil } -func (ec *executionContext) _Query_deferCase2(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_deferCase2(ctx, field) +func (ec *executionContext) _Query_deferMultiple(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_deferMultiple(ctx, field) if err != nil { return graphql.Null } @@ -10541,7 +10541,7 @@ func (ec *executionContext) _Query_deferCase2(ctx context.Context, field graphql }() resTmp := ec._fieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DeferCase2(rctx) + return ec.resolvers.Query().DeferMultiple(rctx) }) if resTmp == nil { @@ -10552,7 +10552,7 @@ func (ec *executionContext) _Query_deferCase2(ctx context.Context, field graphql return ec.marshalODeferModel2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋcodegenᚋtestserverᚋsinglefileᚐDeferModelᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_deferCase2(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Query_deferMultiple(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, @@ -20934,7 +20934,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "deferCase1": + case "deferSingle": field := field innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { @@ -20943,7 +20943,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_deferCase1(ctx, field) + res = ec._Query_deferSingle(ctx, field) return res } @@ -20953,7 +20953,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "deferCase2": + case "deferMultiple": field := field innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { @@ -20962,7 +20962,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_deferCase2(ctx, field) + res = ec._Query_deferMultiple(ctx, field) return res } diff --git a/codegen/testserver/singlefile/resolver.go b/codegen/testserver/singlefile/resolver.go index cb022afa836..943e04506b0 100644 --- a/codegen/testserver/singlefile/resolver.go +++ b/codegen/testserver/singlefile/resolver.go @@ -197,13 +197,13 @@ func (r *queryResolver) DefaultParameters(ctx context.Context, falsyBoolean *boo panic("not implemented") } -// DeferCase1 is the resolver for the deferCase1 field. -func (r *queryResolver) DeferCase1(ctx context.Context) (*DeferModel, error) { +// DeferSingle is the resolver for the deferSingle field. +func (r *queryResolver) DeferSingle(ctx context.Context) (*DeferModel, error) { panic("not implemented") } -// DeferCase2 is the resolver for the deferCase2 field. -func (r *queryResolver) DeferCase2(ctx context.Context) ([]*DeferModel, error) { +// DeferMultiple is the resolver for the deferMultiple field. +func (r *queryResolver) DeferMultiple(ctx context.Context) ([]*DeferModel, error) { panic("not implemented") } diff --git a/codegen/testserver/singlefile/stub.go b/codegen/testserver/singlefile/stub.go index 41552681fdf..89252ba36c3 100644 --- a/codegen/testserver/singlefile/stub.go +++ b/codegen/testserver/singlefile/stub.go @@ -71,8 +71,8 @@ type Stub struct { DeprecatedField func(ctx context.Context) (string, error) Overlapping func(ctx context.Context) (*OverlappingFields, error) DefaultParameters func(ctx context.Context, falsyBoolean *bool, truthyBoolean *bool) (*DefaultParametersMirror, error) - DeferCase1 func(ctx context.Context) (*DeferModel, error) - DeferCase2 func(ctx context.Context) ([]*DeferModel, error) + DeferSingle func(ctx context.Context) (*DeferModel, error) + DeferMultiple func(ctx context.Context) ([]*DeferModel, error) DirectiveArg func(ctx context.Context, arg string) (*string, error) DirectiveNullableArg func(ctx context.Context, arg *int, arg2 *int, arg3 *string) (*string, error) DirectiveSingleNullableArg func(ctx context.Context, arg1 *string) (*string, error) @@ -352,11 +352,11 @@ func (r *stubQuery) Overlapping(ctx context.Context) (*OverlappingFields, error) func (r *stubQuery) DefaultParameters(ctx context.Context, falsyBoolean *bool, truthyBoolean *bool) (*DefaultParametersMirror, error) { return r.QueryResolver.DefaultParameters(ctx, falsyBoolean, truthyBoolean) } -func (r *stubQuery) DeferCase1(ctx context.Context) (*DeferModel, error) { - return r.QueryResolver.DeferCase1(ctx) +func (r *stubQuery) DeferSingle(ctx context.Context) (*DeferModel, error) { + return r.QueryResolver.DeferSingle(ctx) } -func (r *stubQuery) DeferCase2(ctx context.Context) ([]*DeferModel, error) { - return r.QueryResolver.DeferCase2(ctx) +func (r *stubQuery) DeferMultiple(ctx context.Context) ([]*DeferModel, error) { + return r.QueryResolver.DeferMultiple(ctx) } func (r *stubQuery) DirectiveArg(ctx context.Context, arg string) (*string, error) { return r.QueryResolver.DirectiveArg(ctx, arg) From e04bbb26e3b1292cd6bc38ddc12fc577fcc706e0 Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Tue, 26 Nov 2024 16:13:49 -0500 Subject: [PATCH 02/11] expand test cases --- codegen/testserver/singlefile/defer_test.go | 234 +++++++++++++++++--- 1 file changed, 204 insertions(+), 30 deletions(-) diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index 7576b725801..0663daa9667 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -61,25 +61,6 @@ func TestDefer(t *testing.T) { }, nil } - deferSingleQuery := `query testDefer { - deferSingle { - id - name - ... @defer(label: "values") { - values - } - } - }` - deferMultipleQuery := `query testDefer { - deferMultiple { - id - name - ... @defer(label: "values") { - values - } - } - }` - type deferModel struct { Id string Name string @@ -131,8 +112,125 @@ func TestDefer(t *testing.T) { expectedDeferredResponses []sseDeferredResponse }{ { - name: "defer single", - query: deferSingleQuery, + name: "defer single", + query: `query testDefer { + deferSingle { + id + name + ... @defer { + values + } + } +}`, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedDeferredResponses: []sseDeferredResponse{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Path: []any{"deferSingle"}, + }, + }, + }, + { + name: "defer single using fragment type", + query: `query testDefer { + deferSingle { + id + name + ... on DeferModel @defer { + values + } + } +}`, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedDeferredResponses: []sseDeferredResponse{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Path: []any{"deferSingle"}, + }, + }, + }, + { + name: "defer single with label", + query: `query testDefer { + deferSingle { + id + name + ... @defer(label: "test label") { + values + } + } +}`, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedDeferredResponses: []sseDeferredResponse{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferSingle"}, + }, + }, + }, + { + name: "defer single when if arg is true", + query: `query testDefer { + deferSingle { + id + name + ... @defer(if: true, label: "test label") { + values + } + } +}`, expectedInitialResponse: response[struct { DeferSingle deferModel }]{ @@ -154,14 +252,47 @@ func TestDefer(t *testing.T) { }{ Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - Label: "values", + Label: "test label", Path: []any{"deferSingle"}, }, }, }, { - name: "defer multiple", - query: deferMultipleQuery, + name: "defer single when if arg is false", + query: `query testDefer { + deferSingle { + id + name + ... @defer(if: false) { + values + } + } +}`, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + }, + }, + }, + { + name: "defer multiple", + query: `query testDefer { + deferMultiple { + id + name + ... @defer (label: "test label") { + values + } + } +}`, expectedInitialResponse: response[struct { DeferMultiple []deferModel }]{ @@ -195,7 +326,7 @@ func TestDefer(t *testing.T) { }{ Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - Label: "values", + Label: "test label", Path: []any{"deferMultiple", float64(0)}, }, { @@ -204,7 +335,7 @@ func TestDefer(t *testing.T) { }{ Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - Label: "values", + Label: "test label", Path: []any{"deferMultiple", float64(1)}, }, { @@ -213,21 +344,64 @@ func TestDefer(t *testing.T) { }{ Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - Label: "values", + Label: "test label", Path: []any{"deferMultiple", float64(2)}, }, }, }, + { + name: "defer multiple when if arg is false", + query: `query testDefer { + deferMultiple { + id + name + ... @defer(label: "test label", if: false) { + values + } + } +}`, + expectedInitialResponse: response[struct { + DeferMultiple []deferModel + }]{ + Data: struct { + DeferMultiple []deferModel + }{ + DeferMultiple: []deferModel{ + { + Id: "1", + Name: "Defer test 1", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + { + Id: "2", + Name: "Defer test 2", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + { + Id: "3", + Name: "Defer test 3", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + }, + }, + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - initRespT := reflect.TypeOf(tc.expectedInitialResponse) - sse := c.SSE(context.Background(), tc.query) - resp := reflect.New(initRespT).Elem().Interface() + + resT := reflect.TypeOf(tc.expectedInitialResponse) + resE := reflect.New(resT).Elem() + resp := resE.Interface() require.NoError(t, sse.Next(&resp)) assert.Equal(t, tc.expectedInitialResponse, resp) + // If there are no deferred responses, we can stop here. + if !resE.FieldByName("HasNext").Bool() && len(tc.expectedDeferredResponses) == 0 { + return + } + deferredResponses := make([]sseDeferredResponse, 0) for { var valueResp sseDeferredResponse From 64fbb3167dafec43d19aff290363865990489a69 Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Wed, 27 Nov 2024 00:17:55 -0500 Subject: [PATCH 03/11] wip --- client/incremental.go | 197 +++++++ codegen/testserver/singlefile/defer_test.go | 561 +++++++++++++------- 2 files changed, 553 insertions(+), 205 deletions(-) create mode 100644 client/incremental.go diff --git a/client/incremental.go b/client/incremental.go new file mode 100644 index 00000000000..d42d4cc0c35 --- /dev/null +++ b/client/incremental.go @@ -0,0 +1,197 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "io" + "mime" + "mime/multipart" + "net/http" + "net/http/httptest" +) + +type Incremental struct { + Close func() error + Next func(response any) error +} + +type IncrementalInitialResponse = struct { + Data any `json:"data"` + Label string `json:"label"` + Path []any `json:"path"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` +} + +type IncrementalPendingData struct { + ID string `json:"id"` + Path []any `json:"path"` + Label string `json:"label"` +} + +type IncrementalCompletedData struct { + ID string `json:"id"` +} + +type IncrementalDataResponse struct { + // ID string `json:"id"` + // Items []any `json:"items"` + Data any `json:"data"` + Label string `json:"label"` + Path []any `json:"path"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` +} + +type IncrementalResponse struct { + // Pending []IncrementalPendingData `json:"pending"` + // Completed []IncrementalCompletedData `json:"completed"` + Incremental []IncrementalDataResponse `json:"incremental"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` +} + +func errorIncremental(err error) *Incremental { + return &Incremental{ + Close: func() error { return nil }, + Next: func(response any) error { + return err + }, + } +} + +func (p *Client) Incremental(ctx context.Context, query string, options ...Option) *Incremental { + r, err := p.newRequest(query, options...) + if err != nil { + return errorIncremental(fmt.Errorf("request: %w", err)) + } + r.Header.Set("Accept", "multipart/mixed") + + w := httptest.NewRecorder() + p.h.ServeHTTP(w, r) + + res := w.Result() + if res.StatusCode >= http.StatusBadRequest { + return errorIncremental(fmt.Errorf("http %d: %s", w.Code, w.Body.String())) + } + mediaType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type")) + if err != nil { + return errorIncremental(fmt.Errorf("parse content-type: %w", err)) + } + if mediaType != "multipart/mixed" { + return errorIncremental(fmt.Errorf("expected content-type multipart/mixed, got %s", mediaType)) + } + boundary, ok := params["boundary"] + if !ok || boundary == "" { + return errorIncremental(fmt.Errorf("expected boundary in content-type")) + } + deferSpec, ok := params["deferspec"] + if !ok || deferSpec == "" { + return errorIncremental(fmt.Errorf("expected deferSpec in content-type")) + } + + errCh := make(chan error, 1) + initCh := make(chan IncrementalInitialResponse) + nextCh := make(chan IncrementalResponse) + + ctx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + defer res.Body.Close() + + initialResponse := true + mr := multipart.NewReader(res.Body, boundary) + for { + type nextPart struct { + *multipart.Part + Err error + } + + nextPartCh := make(chan nextPart) + go func() { + var next nextPart + next.Part, next.Err = mr.NextPart() + nextPartCh <- next + }() + + var next nextPart + select { + case <-ctx.Done(): + if err := ctx.Err(); err != nil { + errCh <- fmt.Errorf("context: %w", err) + } + return + case next = <-nextPartCh: + } + + if next.Err == io.EOF { + break + } + if next.Err != nil { + errCh <- fmt.Errorf("next part: %w", next.Err) + return + } + if ct := next.Header.Get("Content-Type"); ct != "application/json" { + errCh <- fmt.Errorf(`expected content-type "application/json", got %q`, ct) + return + } + + if initialResponse { + initialResponse = false + var data IncrementalInitialResponse + if err = json.NewDecoder(next.Part).Decode(&data); err != nil { + errCh <- fmt.Errorf("decode part: %w", err) + return + } + initCh <- data + close(initCh) + } else { + var data IncrementalResponse + if err = json.NewDecoder(next.Part).Decode(&data); err != nil { + errCh <- fmt.Errorf("decode part: %w", err) + return + } + nextCh <- data + } + } + }() + + return &Incremental{ + Close: func() error { + cancel() + return nil + }, + Next: func(response any) error { + var data any + var rawErrors json.RawMessage + + select { + case nextErr := <-errCh: + return nextErr + case initData, ok := <-initCh: + if !ok { + select { + case nextErr := <-errCh: + return nextErr + case nextData := <-nextCh: + data = nextData + rawErrors = nextData.Errors + } + } else { + data = initData + rawErrors = initData.Errors + } + } + // we want to unpack even if there is an error, so we can see partial responses + unpackErr := unpack(data, response, p.dc) + if rawErrors != nil { + return RawJsonError{rawErrors} + } + return unpackErr + }, + } +} diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index 0663daa9667..6f341a0da36 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -25,6 +25,7 @@ func TestDefer(t *testing.T) { srv := handler.New(NewExecutableSchema(Config{Resolvers: resolvers})) srv.AddTransport(transport.SSE{}) + srv.AddTransport(transport.MultipartMixed{}) c := client.New(srv) @@ -66,6 +67,7 @@ func TestDefer(t *testing.T) { Name string Values []string } + type response[T any] struct { Data T Label string `json:"label"` @@ -74,6 +76,7 @@ func TestDefer(t *testing.T) { Errors json.RawMessage `json:"errors"` Extensions map[string]any `json:"extensions"` } + type sseDeferredResponse struct { Data struct { Values []string `json:"values"` @@ -85,6 +88,15 @@ func TestDefer(t *testing.T) { Extensions map[string]any `json:"extensions"` } + type incrementalDeferredResponse struct { + Incremental []response[struct { + Values []string `json:"values"` + }] `json:"incremental"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` + } + pathStringer := func(path []any) string { var kb strings.Builder for i, part := range path { @@ -104,16 +116,16 @@ func TestDefer(t *testing.T) { return kb.String() } - t.Run("using SSE", func(t *testing.T) { - cases := []struct { - name string - query string - expectedInitialResponse interface{} - expectedDeferredResponses []sseDeferredResponse - }{ - { - name: "defer single", - query: `query testDefer { + cases := []struct { + name string + query string + expectedInitialResponse interface{} + expectedSSEDeferredResponses []sseDeferredResponse + expectedIncrementalResponses []incrementalDeferredResponse + }{ + { + name: "defer single", + query: `query testDefer { deferSingle { id name @@ -122,34 +134,50 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { DeferSingle deferModel - }]{ + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedSSEDeferredResponses: []sseDeferredResponse{ + { Data: struct { - DeferSingle deferModel + Values []string `json:"values"` }{ - DeferSingle: deferModel{ - Id: "1", - Name: "Defer test 1", - Values: nil, - }, + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - HasNext: true, + Path: []any{"deferSingle"}, }, - expectedDeferredResponses: []sseDeferredResponse{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + expectedIncrementalResponses: []incrementalDeferredResponse{ + { + Incremental: []response[struct { + Values []string `json:"values"` + }]{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Path: []any{"deferSingle"}, }, - Path: []any{"deferSingle"}, }, }, }, - { - name: "defer single using fragment type", - query: `query testDefer { + }, + { + name: "defer single using fragment type", + query: `query testDefer { deferSingle { id name @@ -158,34 +186,50 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { DeferSingle deferModel - }]{ + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedSSEDeferredResponses: []sseDeferredResponse{ + { Data: struct { - DeferSingle deferModel + Values []string `json:"values"` }{ - DeferSingle: deferModel{ - Id: "1", - Name: "Defer test 1", - Values: nil, - }, + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - HasNext: true, + Path: []any{"deferSingle"}, }, - expectedDeferredResponses: []sseDeferredResponse{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + expectedIncrementalResponses: []incrementalDeferredResponse{ + { + Incremental: []response[struct { + Values []string `json:"values"` + }]{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Path: []any{"deferSingle"}, }, - Path: []any{"deferSingle"}, }, }, }, - { - name: "defer single with label", - query: `query testDefer { + }, + { + name: "defer single with label", + query: `query testDefer { deferSingle { id name @@ -194,35 +238,52 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { DeferSingle deferModel - }]{ + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedSSEDeferredResponses: []sseDeferredResponse{ + { Data: struct { - DeferSingle deferModel + Values []string `json:"values"` }{ - DeferSingle: deferModel{ - Id: "1", - Name: "Defer test 1", - Values: nil, - }, + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - HasNext: true, + Label: "test label", + Path: []any{"deferSingle"}, }, - expectedDeferredResponses: []sseDeferredResponse{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + expectedIncrementalResponses: []incrementalDeferredResponse{ + { + Incremental: []response[struct { + Values []string `json:"values"` + }]{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferSingle"}, }, - Label: "test label", - Path: []any{"deferSingle"}, }, }, }, - { - name: "defer single when if arg is true", - query: `query testDefer { + }, + { + name: "defer single when if arg is true", + query: `query testDefer { deferSingle { id name @@ -231,35 +292,52 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { DeferSingle deferModel - }]{ + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedSSEDeferredResponses: []sseDeferredResponse{ + { Data: struct { - DeferSingle deferModel + Values []string `json:"values"` }{ - DeferSingle: deferModel{ - Id: "1", - Name: "Defer test 1", - Values: nil, - }, + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - HasNext: true, + Label: "test label", + Path: []any{"deferSingle"}, }, - expectedDeferredResponses: []sseDeferredResponse{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + expectedIncrementalResponses: []incrementalDeferredResponse{ + { + Incremental: []response[struct { + Values []string `json:"values"` + }]{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferSingle"}, }, - Label: "test label", - Path: []any{"deferSingle"}, }, }, }, - { - name: "defer single when if arg is false", - query: `query testDefer { + }, + { + name: "defer single when if arg is false", + query: `query testDefer { deferSingle { id name @@ -268,23 +346,23 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { DeferSingle deferModel - }]{ - Data: struct { - DeferSingle deferModel - }{ - DeferSingle: deferModel{ - Id: "1", - Name: "Defer test 1", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, }, }, - { - name: "defer multiple", - query: `query testDefer { + }, + { + name: "defer multiple", + query: `query testDefer { deferMultiple { id name @@ -293,65 +371,100 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferMultiple []deferModel + }]{ + Data: struct { DeferMultiple []deferModel - }]{ + }{ + DeferMultiple: []deferModel{ + { + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + { + Id: "2", + Name: "Defer test 2", + Values: nil, + }, + { + Id: "3", + Name: "Defer test 3", + Values: nil, + }, + }, + }, + HasNext: true, + }, + expectedSSEDeferredResponses: []sseDeferredResponse{ + { Data: struct { - DeferMultiple []deferModel + Values []string `json:"values"` }{ - DeferMultiple: []deferModel{ - { - Id: "1", - Name: "Defer test 1", - Values: nil, - }, - { - Id: "2", - Name: "Defer test 2", - Values: nil, - }, - { - Id: "3", - Name: "Defer test 3", - Values: nil, - }, - }, + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - HasNext: true, + Label: "test label", + Path: []any{"deferMultiple", float64(0)}, }, - expectedDeferredResponses: []sseDeferredResponse{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferMultiple", float64(0)}, + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferMultiple", float64(1)}, + Label: "test label", + Path: []any{"deferMultiple", float64(1)}, + }, + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + Label: "test label", + Path: []any{"deferMultiple", float64(2)}, + }, + }, + expectedIncrementalResponses: []incrementalDeferredResponse{ + { + Incremental: []response[struct { + Values []string `json:"values"` + }]{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferMultiple", float64(0)}, + }, + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferMultiple", float64(1)}, + }, + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferMultiple", float64(2)}, }, - Label: "test label", - Path: []any{"deferMultiple", float64(2)}, }, }, }, - { - name: "defer multiple when if arg is false", - query: `query testDefer { + }, + { + name: "defer multiple when if arg is false", + query: `query testDefer { deferMultiple { id name @@ -360,72 +473,110 @@ func TestDefer(t *testing.T) { } } }`, - expectedInitialResponse: response[struct { + expectedInitialResponse: response[struct { + DeferMultiple []deferModel + }]{ + Data: struct { DeferMultiple []deferModel - }]{ - Data: struct { - DeferMultiple []deferModel - }{ - DeferMultiple: []deferModel{ - { - Id: "1", - Name: "Defer test 1", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - { - Id: "2", - Name: "Defer test 2", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - { - Id: "3", - Name: "Defer test 3", - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, + }{ + DeferMultiple: []deferModel{ + { + Id: "1", + Name: "Defer test 1", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + { + Id: "2", + Name: "Defer test 2", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + { + Id: "3", + Name: "Defer test 3", + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, }, }, }, }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - sse := c.SSE(context.Background(), tc.query) + }, + } + for _, tc := range cases { + t.Run("using SSE/"+tc.name, func(t *testing.T) { + resT := reflect.TypeOf(tc.expectedInitialResponse) + resE := reflect.New(resT).Elem() + resp := resE.Interface() - resT := reflect.TypeOf(tc.expectedInitialResponse) - resE := reflect.New(resT).Elem() - resp := resE.Interface() - require.NoError(t, sse.Next(&resp)) - assert.Equal(t, tc.expectedInitialResponse, resp) + read := c.SSE(context.Background(), tc.query) + require.NoError(t, read.Next(&resp)) + assert.Equal(t, tc.expectedInitialResponse, resp) - // If there are no deferred responses, we can stop here. - if !resE.FieldByName("HasNext").Bool() && len(tc.expectedDeferredResponses) == 0 { - return - } + // If there are no deferred responses, we can stop here. + if !resE.FieldByName("HasNext").Bool() && len(tc.expectedSSEDeferredResponses) == 0 { + return + } - deferredResponses := make([]sseDeferredResponse, 0) - for { - var valueResp sseDeferredResponse - require.NoError(t, sse.Next(&valueResp)) + deferredResponses := make([]sseDeferredResponse, 0) + for { + var valueResp sseDeferredResponse + require.NoError(t, read.Next(&valueResp)) - if !valueResp.HasNext { - deferredResponses = append(deferredResponses, valueResp) - break - } else { - // Remove HasNext from comparison: we don't know the order they will be - // delivered in, and so this can't be known in the setup. But if HasNext - // does not work right we will either error out or get too few - // responses, so it's still checked. - valueResp.HasNext = false - deferredResponses = append(deferredResponses, valueResp) - } + if !valueResp.HasNext { + deferredResponses = append(deferredResponses, valueResp) + break + } else { + // Remove HasNext from comparison: we don't know the order they will be + // delivered in, and so this can't be known in the setup. But if HasNext + // does not work right we will either error out or get too few + // responses, so it's still checked. + valueResp.HasNext = false + deferredResponses = append(deferredResponses, valueResp) } - require.NoError(t, sse.Close()) + } + require.NoError(t, read.Close()) - slices.SortFunc(deferredResponses, func(a, b sseDeferredResponse) int { - return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) - }) - assert.Equal(t, tc.expectedDeferredResponses, deferredResponses) + slices.SortFunc(deferredResponses, func(a, b sseDeferredResponse) int { + return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) }) - } - }) + assert.Equal(t, tc.expectedSSEDeferredResponses, deferredResponses) + }) + + t.Run("using incremental delivery/"+tc.name, func(t *testing.T) { + resT := reflect.TypeOf(tc.expectedInitialResponse) + resE := reflect.New(resT).Elem() + resp := resE.Interface() + + read := c.Incremental(context.Background(), tc.query) + require.NoError(t, read.Next(&resp)) + assert.Equal(t, tc.expectedInitialResponse, resp) + + // If there are no deferred responses, we can stop here. + if !reflect.ValueOf(resp).FieldByName("HasNext").Bool() && len(tc.expectedIncrementalResponses) == 0 { + return + } + + deferredResponses := make([]incrementalDeferredResponse, 0) + for { + var valueResp incrementalDeferredResponse + require.NoError(t, read.Next(&valueResp)) + + if !valueResp.HasNext { + deferredResponses = append(deferredResponses, valueResp) + break + } else { + // Remove HasNext from comparison: we don't know the order they will be + // delivered in, and so this can't be known in the setup. But if HasNext + // does not work right we will either error out or get too few + // responses, so it's still checked. + valueResp.HasNext = false + deferredResponses = append(deferredResponses, valueResp) + } + } + require.NoError(t, read.Close()) + + // slices.SortFunc(deferredResponses, func(a, b incrementalDeferredResponse) int { + // return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) + // }) + assert.Equal(t, tc.expectedIncrementalResponses, deferredResponses) + }) + } } From 071c90f21e9f5e48410c7bb0aacdf6e882dcb777 Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Wed, 27 Nov 2024 12:35:11 -0500 Subject: [PATCH 04/11] dig in to specs and solidify client and test implementations --- client/incremental.go | 179 ++++++++-------- codegen/testserver/singlefile/defer_test.go | 191 +++++------------- .../handler/transport/http_multipart_mixed.go | 13 ++ 3 files changed, 143 insertions(+), 240 deletions(-) diff --git a/client/incremental.go b/client/incremental.go index d42d4cc0c35..dfbfa017d86 100644 --- a/client/incremental.go +++ b/client/incremental.go @@ -12,11 +12,19 @@ import ( ) type Incremental struct { - Close func() error - Next func(response any) error + close func() error + next func(response any) error } -type IncrementalInitialResponse = struct { +func (i *Incremental) Close() error { + return i.close() +} + +func (i *Incremental) Next(response any) error { + return i.next(response) +} + +type IncrementalInitialResponse struct { Data any `json:"data"` Label string `json:"label"` Path []any `json:"path"` @@ -25,19 +33,12 @@ type IncrementalInitialResponse = struct { Extensions map[string]any `json:"extensions"` } -type IncrementalPendingData struct { - ID string `json:"id"` - Path []any `json:"path"` - Label string `json:"label"` -} - -type IncrementalCompletedData struct { - ID string `json:"id"` -} +type IncrementalData struct { + // Support for "items" for @stream is not yet available, only "data" for + // @defer, as per the 2023 spec. Similarly, this retains a more complete + // list of fields, but not "id," and represents a mid-point between the + // 2022 and 2023 specs. -type IncrementalDataResponse struct { - // ID string `json:"id"` - // Items []any `json:"items"` Data any `json:"data"` Label string `json:"label"` Path []any `json:"path"` @@ -47,23 +48,38 @@ type IncrementalDataResponse struct { } type IncrementalResponse struct { - // Pending []IncrementalPendingData `json:"pending"` - // Completed []IncrementalCompletedData `json:"completed"` - Incremental []IncrementalDataResponse `json:"incremental"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` + // Does not include the pending or completed fields from the 2023 spec. + + Incremental []IncrementalData `json:"incremental"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` } func errorIncremental(err error) *Incremental { return &Incremental{ - Close: func() error { return nil }, - Next: func(response any) error { + close: func() error { return nil }, + next: func(response any) error { return err }, } } +// Incremental returns a GraphQL response handler for the current GQLGen +// implementation of the [incremental delivery over HTTP spec]. This is +// an alternate approach to server-sent events that provides "streaming" +// responses triggered by the use of @stream or @defer. To that end, the +// client retains the interface of the handler returned from Client.SSE. +// +// Incremental delivery using multipart/mixed is just the structure of +// the response: the payloads are specified by the defer-stream spec, +// which are in transition. For more detail, see the links in the +// definition for transport.MultipartMixed. +// +// The Incremental handler is not safe for concurrent use or for +// production use at all. +// +// [incremental delivery over HTTP spec]: https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md func (p *Client) Incremental(ctx context.Context, query string, options ...Option) *Incremental { r, err := p.newRequest(query, options...) if err != nil { @@ -85,27 +101,39 @@ func (p *Client) Incremental(ctx context.Context, query string, options ...Optio if mediaType != "multipart/mixed" { return errorIncremental(fmt.Errorf("expected content-type multipart/mixed, got %s", mediaType)) } - boundary, ok := params["boundary"] - if !ok || boundary == "" { - return errorIncremental(fmt.Errorf("expected boundary in content-type")) - } + + // TODO: worth checking the deferSpec either to confirm this client + // supports it exactly, or simply to make sure it is within some + // expected range. deferSpec, ok := params["deferspec"] if !ok || deferSpec == "" { return errorIncremental(fmt.Errorf("expected deferSpec in content-type")) } - errCh := make(chan error, 1) - initCh := make(chan IncrementalInitialResponse) - nextCh := make(chan IncrementalResponse) + boundary, ok := params["boundary"] + if !ok || boundary == "" { + return errorIncremental(fmt.Errorf("expected boundary in content-type")) + } + mr := multipart.NewReader(res.Body, boundary) + + ctx, cancel := context.WithCancelCause(ctx) + initial := true + + return &Incremental{ + close: func() error { + cancel(context.Canceled) + return nil + }, + next: func(response any) (err error) { + defer func() { + if err != nil { + cancel(err) + } + }() - ctx, cancel := context.WithCancel(ctx) - go func() { - defer cancel() - defer res.Body.Close() + var data any + var rawErrors json.RawMessage - initialResponse := true - mr := multipart.NewReader(res.Body, boundary) - for { type nextPart struct { *multipart.Part Err error @@ -121,77 +149,40 @@ func (p *Client) Incremental(ctx context.Context, query string, options ...Optio var next nextPart select { case <-ctx.Done(): - if err := ctx.Err(); err != nil { - errCh <- fmt.Errorf("context: %w", err) - } - return + return ctx.Err() case next = <-nextPartCh: } if next.Err == io.EOF { - break + cancel(context.Canceled) + return nil } - if next.Err != nil { - errCh <- fmt.Errorf("next part: %w", next.Err) - return + if err = next.Err; err != nil { + return err } if ct := next.Header.Get("Content-Type"); ct != "application/json" { - errCh <- fmt.Errorf(`expected content-type "application/json", got %q`, ct) - return + err = fmt.Errorf(`expected content-type "application/json", got %q`, ct) + return err } - if initialResponse { - initialResponse = false - var data IncrementalInitialResponse - if err = json.NewDecoder(next.Part).Decode(&data); err != nil { - errCh <- fmt.Errorf("decode part: %w", err) - return - } - initCh <- data - close(initCh) + if initial { + initial = false + data = IncrementalInitialResponse{} } else { - var data IncrementalResponse - if err = json.NewDecoder(next.Part).Decode(&data); err != nil { - errCh <- fmt.Errorf("decode part: %w", err) - return - } - nextCh <- data + data = IncrementalResponse{} } - } - }() - - return &Incremental{ - Close: func() error { - cancel() - return nil - }, - Next: func(response any) error { - var data any - var rawErrors json.RawMessage - - select { - case nextErr := <-errCh: - return nextErr - case initData, ok := <-initCh: - if !ok { - select { - case nextErr := <-errCh: - return nextErr - case nextData := <-nextCh: - data = nextData - rawErrors = nextData.Errors - } - } else { - data = initData - rawErrors = initData.Errors - } + if err = json.NewDecoder(next.Part).Decode(&data); err != nil { + return err } - // we want to unpack even if there is an error, so we can see partial responses - unpackErr := unpack(data, response, p.dc) + + // We want to unpack even if there is an error, so we can see partial + // responses. + err = unpack(data, response, p.dc) if rawErrors != nil { - return RawJsonError{rawErrors} + err = RawJsonError{rawErrors} + return err } - return unpackErr + return err }, } } diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index 6f341a0da36..594d0c6664a 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -77,24 +77,15 @@ func TestDefer(t *testing.T) { Extensions map[string]any `json:"extensions"` } - type sseDeferredResponse struct { - Data struct { - Values []string `json:"values"` - } - Label string `json:"label"` - Path []any `json:"path"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` - } + type deferredResponse response[struct { + Values []string `json:"values"` + }] type incrementalDeferredResponse struct { - Incremental []response[struct { - Values []string `json:"values"` - }] `json:"incremental"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` + Incremental []deferredResponse `json:"incremental"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` } pathStringer := func(path []any) string { @@ -117,11 +108,10 @@ func TestDefer(t *testing.T) { } cases := []struct { - name string - query string - expectedInitialResponse interface{} - expectedSSEDeferredResponses []sseDeferredResponse - expectedIncrementalResponses []incrementalDeferredResponse + name string + query string + expectedInitialResponse interface{} + expectedDeferredResponses []deferredResponse }{ { name: "defer single", @@ -148,7 +138,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedSSEDeferredResponses: []sseDeferredResponse{ + expectedDeferredResponses: []deferredResponse{ { Data: struct { Values []string `json:"values"` @@ -158,22 +148,6 @@ func TestDefer(t *testing.T) { Path: []any{"deferSingle"}, }, }, - expectedIncrementalResponses: []incrementalDeferredResponse{ - { - Incremental: []response[struct { - Values []string `json:"values"` - }]{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Path: []any{"deferSingle"}, - }, - }, - }, - }, }, { name: "defer single using fragment type", @@ -200,7 +174,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedSSEDeferredResponses: []sseDeferredResponse{ + expectedDeferredResponses: []deferredResponse{ { Data: struct { Values []string `json:"values"` @@ -210,22 +184,6 @@ func TestDefer(t *testing.T) { Path: []any{"deferSingle"}, }, }, - expectedIncrementalResponses: []incrementalDeferredResponse{ - { - Incremental: []response[struct { - Values []string `json:"values"` - }]{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Path: []any{"deferSingle"}, - }, - }, - }, - }, }, { name: "defer single with label", @@ -252,7 +210,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedSSEDeferredResponses: []sseDeferredResponse{ + expectedDeferredResponses: []deferredResponse{ { Data: struct { Values []string `json:"values"` @@ -263,23 +221,6 @@ func TestDefer(t *testing.T) { Path: []any{"deferSingle"}, }, }, - expectedIncrementalResponses: []incrementalDeferredResponse{ - { - Incremental: []response[struct { - Values []string `json:"values"` - }]{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferSingle"}, - }, - }, - }, - }, }, { name: "defer single when if arg is true", @@ -306,7 +247,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedSSEDeferredResponses: []sseDeferredResponse{ + expectedDeferredResponses: []deferredResponse{ { Data: struct { Values []string `json:"values"` @@ -317,23 +258,6 @@ func TestDefer(t *testing.T) { Path: []any{"deferSingle"}, }, }, - expectedIncrementalResponses: []incrementalDeferredResponse{ - { - Incremental: []response[struct { - Values []string `json:"values"` - }]{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferSingle"}, - }, - }, - }, - }, }, { name: "defer single when if arg is false", @@ -397,7 +321,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedSSEDeferredResponses: []sseDeferredResponse{ + expectedDeferredResponses: []deferredResponse{ { Data: struct { Values []string `json:"values"` @@ -426,41 +350,6 @@ func TestDefer(t *testing.T) { Path: []any{"deferMultiple", float64(2)}, }, }, - expectedIncrementalResponses: []incrementalDeferredResponse{ - { - Incremental: []response[struct { - Values []string `json:"values"` - }]{ - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferMultiple", float64(0)}, - }, - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferMultiple", float64(1)}, - }, - { - Data: struct { - Values []string `json:"values"` - }{ - Values: []string{"test defer 1", "test defer 2", "test defer 3"}, - }, - Label: "test label", - Path: []any{"deferMultiple", float64(2)}, - }, - }, - }, - }, }, { name: "defer multiple when if arg is false", @@ -511,13 +400,13 @@ func TestDefer(t *testing.T) { assert.Equal(t, tc.expectedInitialResponse, resp) // If there are no deferred responses, we can stop here. - if !resE.FieldByName("HasNext").Bool() && len(tc.expectedSSEDeferredResponses) == 0 { + if !resE.FieldByName("HasNext").Bool() && len(tc.expectedDeferredResponses) == 0 { return } - deferredResponses := make([]sseDeferredResponse, 0) + deferredResponses := make([]deferredResponse, 0) for { - var valueResp sseDeferredResponse + var valueResp deferredResponse require.NoError(t, read.Next(&valueResp)) if !valueResp.HasNext { @@ -534,10 +423,10 @@ func TestDefer(t *testing.T) { } require.NoError(t, read.Close()) - slices.SortFunc(deferredResponses, func(a, b sseDeferredResponse) int { + slices.SortFunc(deferredResponses, func(a, b deferredResponse) int { return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) }) - assert.Equal(t, tc.expectedSSEDeferredResponses, deferredResponses) + assert.Equal(t, tc.expectedDeferredResponses, deferredResponses) }) t.Run("using incremental delivery/"+tc.name, func(t *testing.T) { @@ -550,33 +439,43 @@ func TestDefer(t *testing.T) { assert.Equal(t, tc.expectedInitialResponse, resp) // If there are no deferred responses, we can stop here. - if !reflect.ValueOf(resp).FieldByName("HasNext").Bool() && len(tc.expectedIncrementalResponses) == 0 { + if !reflect.ValueOf(resp).FieldByName("HasNext").Bool() && len(tc.expectedDeferredResponses) == 0 { return } - deferredResponses := make([]incrementalDeferredResponse, 0) + deferredIncrementalData := make([]deferredResponse, 0) for { var valueResp incrementalDeferredResponse require.NoError(t, read.Next(&valueResp)) + assert.Empty(t, valueResp.Errors) + assert.Empty(t, valueResp.Extensions) + + // Extract the incremental data from the response. + // + // FIXME: currently the HasNext field does not describe the state of the + // delivery as bounded by the associated path, but rather the state of + // the operation as a whole. This makes it impossible to determine it + // from the response, so we can not define it ahead of time. + // + // It is also questionable that the incremental data objects should + // include hasNext, so for now we remove them from assertion. Once we + // align on the spec we must update this test, as the status of the + // path-bounded delivery should be determinative and can be asserted. + for _, incr := range valueResp.Incremental { + incr.HasNext = false + deferredIncrementalData = append(deferredIncrementalData, incr) + } if !valueResp.HasNext { - deferredResponses = append(deferredResponses, valueResp) break - } else { - // Remove HasNext from comparison: we don't know the order they will be - // delivered in, and so this can't be known in the setup. But if HasNext - // does not work right we will either error out or get too few - // responses, so it's still checked. - valueResp.HasNext = false - deferredResponses = append(deferredResponses, valueResp) } } require.NoError(t, read.Close()) - // slices.SortFunc(deferredResponses, func(a, b incrementalDeferredResponse) int { - // return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) - // }) - assert.Equal(t, tc.expectedIncrementalResponses, deferredResponses) + slices.SortFunc(deferredIncrementalData, func(a, b deferredResponse) int { + return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) + }) + assert.Equal(t, tc.expectedDeferredResponses, deferredIncrementalData) }) } } diff --git a/graphql/handler/transport/http_multipart_mixed.go b/graphql/handler/transport/http_multipart_mixed.go index 6447b286043..c3ac48f9eba 100644 --- a/graphql/handler/transport/http_multipart_mixed.go +++ b/graphql/handler/transport/http_multipart_mixed.go @@ -269,9 +269,22 @@ func (a *multipartResponseAggregator) flush(w http.ResponseWriter) { if len(a.deferResponses) > 0 { writeContentTypeHeader(w) + + // Note: while the 2023 spec that includes "incremental" does not + // explicitly list the fields that should be included as part of the + // incremental object, it shows hasNext only on the response payload + // (marking the status of the operation as a whole), and instead the + // response payload implements pending and complete fields to mark the + // status of the incrementally delivered data. + // + // TODO: use the "HasNext" status of deferResponses items to determine + // the operation status and pending / complete fields, but remove from + // the incremental (deferResponses) object. + var hasNext bool hasNext = a.deferResponses[len(a.deferResponses)-1].HasNext != nil && *a.deferResponses[len(a.deferResponses)-1].HasNext writeIncrementalJson(w, a.deferResponses, hasNext) + // Reset the deferResponses so we don't send them again a.deferResponses = nil } From 9da88c8805b57c0e5d2f1537bbd3dda0ba178d6a Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Wed, 27 Nov 2024 12:59:43 -0500 Subject: [PATCH 05/11] finalize shape of defer tests --- .../{incremental.go => incremental_http.go} | 37 +++++++++--------- codegen/testserver/singlefile/defer_test.go | 38 +++++++++---------- .../handler/transport/http_multipart_mixed.go | 1 - 3 files changed, 39 insertions(+), 37 deletions(-) rename client/{incremental.go => incremental_http.go} (78%) diff --git a/client/incremental.go b/client/incremental_http.go similarity index 78% rename from client/incremental.go rename to client/incremental_http.go index dfbfa017d86..c02f2569836 100644 --- a/client/incremental.go +++ b/client/incremental_http.go @@ -11,16 +11,16 @@ import ( "net/http/httptest" ) -type Incremental struct { +type IncrementalHandler struct { close func() error next func(response any) error } -func (i *Incremental) Close() error { +func (i *IncrementalHandler) Close() error { return i.close() } -func (i *Incremental) Next(response any) error { +func (i *IncrementalHandler) Next(response any) error { return i.next(response) } @@ -56,8 +56,8 @@ type IncrementalResponse struct { Extensions map[string]any `json:"extensions"` } -func errorIncremental(err error) *Incremental { - return &Incremental{ +func errorIncremental(err error) *IncrementalHandler { + return &IncrementalHandler{ close: func() error { return nil }, next: func(response any) error { return err @@ -65,22 +65,25 @@ func errorIncremental(err error) *Incremental { } } -// Incremental returns a GraphQL response handler for the current GQLGen -// implementation of the [incremental delivery over HTTP spec]. This is -// an alternate approach to server-sent events that provides "streaming" -// responses triggered by the use of @stream or @defer. To that end, the -// client retains the interface of the handler returned from Client.SSE. +// IncrementalHTTP returns a GraphQL response handler for the current +// GQLGen implementation of the [incremental delivery over HTTP spec]. +// This spec provides for "streaming" responses triggered by the use of +// @stream or @defer using is an alternate approach to SSE. To that end, +// the client retains the interface of the handler returned from +// Client.SSE. // -// Incremental delivery using multipart/mixed is just the structure of -// the response: the payloads are specified by the defer-stream spec, +// IncrementalHTTP delivery using multipart/mixed is just the structure +// of the response: the payloads are specified by the defer-stream spec, // which are in transition. For more detail, see the links in the -// definition for transport.MultipartMixed. +// definition for transport.MultipartMixed. We use the name +// IncrementalHTTP here to distinguish from the multipart form upload +// (the term "multipart" usually referring to the latter). // -// The Incremental handler is not safe for concurrent use or for -// production use at all. +// IncrementalHandler is not safe for concurrent use, or for production +// use at all. // // [incremental delivery over HTTP spec]: https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md -func (p *Client) Incremental(ctx context.Context, query string, options ...Option) *Incremental { +func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...Option) *IncrementalHandler { r, err := p.newRequest(query, options...) if err != nil { return errorIncremental(fmt.Errorf("request: %w", err)) @@ -119,7 +122,7 @@ func (p *Client) Incremental(ctx context.Context, query string, options ...Optio ctx, cancel := context.WithCancelCause(ctx) initial := true - return &Incremental{ + return &IncrementalHandler{ close: func() error { cancel(context.Canceled) return nil diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index 594d0c6664a..ee80533d6ea 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -77,15 +77,15 @@ func TestDefer(t *testing.T) { Extensions map[string]any `json:"extensions"` } - type deferredResponse response[struct { + type deferredData response[struct { Values []string `json:"values"` }] type incrementalDeferredResponse struct { - Incremental []deferredResponse `json:"incremental"` - HasNext bool `json:"hasNext"` - Errors json.RawMessage `json:"errors"` - Extensions map[string]any `json:"extensions"` + Incremental []deferredData `json:"incremental"` + HasNext bool `json:"hasNext"` + Errors json.RawMessage `json:"errors"` + Extensions map[string]any `json:"extensions"` } pathStringer := func(path []any) string { @@ -111,7 +111,7 @@ func TestDefer(t *testing.T) { name string query string expectedInitialResponse interface{} - expectedDeferredResponses []deferredResponse + expectedDeferredResponses []deferredData }{ { name: "defer single", @@ -138,7 +138,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedDeferredResponses: []deferredResponse{ + expectedDeferredResponses: []deferredData{ { Data: struct { Values []string `json:"values"` @@ -174,7 +174,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedDeferredResponses: []deferredResponse{ + expectedDeferredResponses: []deferredData{ { Data: struct { Values []string `json:"values"` @@ -210,7 +210,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedDeferredResponses: []deferredResponse{ + expectedDeferredResponses: []deferredData{ { Data: struct { Values []string `json:"values"` @@ -247,7 +247,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedDeferredResponses: []deferredResponse{ + expectedDeferredResponses: []deferredData{ { Data: struct { Values []string `json:"values"` @@ -321,7 +321,7 @@ func TestDefer(t *testing.T) { }, HasNext: true, }, - expectedDeferredResponses: []deferredResponse{ + expectedDeferredResponses: []deferredData{ { Data: struct { Values []string `json:"values"` @@ -390,7 +390,7 @@ func TestDefer(t *testing.T) { }, } for _, tc := range cases { - t.Run("using SSE/"+tc.name, func(t *testing.T) { + t.Run(tc.name+"/over SSE", func(t *testing.T) { resT := reflect.TypeOf(tc.expectedInitialResponse) resE := reflect.New(resT).Elem() resp := resE.Interface() @@ -404,9 +404,9 @@ func TestDefer(t *testing.T) { return } - deferredResponses := make([]deferredResponse, 0) + deferredResponses := make([]deferredData, 0) for { - var valueResp deferredResponse + var valueResp deferredData require.NoError(t, read.Next(&valueResp)) if !valueResp.HasNext { @@ -423,18 +423,18 @@ func TestDefer(t *testing.T) { } require.NoError(t, read.Close()) - slices.SortFunc(deferredResponses, func(a, b deferredResponse) int { + slices.SortFunc(deferredResponses, func(a, b deferredData) int { return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) }) assert.Equal(t, tc.expectedDeferredResponses, deferredResponses) }) - t.Run("using incremental delivery/"+tc.name, func(t *testing.T) { + t.Run(tc.name+"/over multipart HTTP", func(t *testing.T) { resT := reflect.TypeOf(tc.expectedInitialResponse) resE := reflect.New(resT).Elem() resp := resE.Interface() - read := c.Incremental(context.Background(), tc.query) + read := c.IncrementalHTTP(context.Background(), tc.query) require.NoError(t, read.Next(&resp)) assert.Equal(t, tc.expectedInitialResponse, resp) @@ -443,7 +443,7 @@ func TestDefer(t *testing.T) { return } - deferredIncrementalData := make([]deferredResponse, 0) + deferredIncrementalData := make([]deferredData, 0) for { var valueResp incrementalDeferredResponse require.NoError(t, read.Next(&valueResp)) @@ -472,7 +472,7 @@ func TestDefer(t *testing.T) { } require.NoError(t, read.Close()) - slices.SortFunc(deferredIncrementalData, func(a, b deferredResponse) int { + slices.SortFunc(deferredIncrementalData, func(a, b deferredData) int { return cmp.Compare(pathStringer(a.Path), pathStringer(b.Path)) }) assert.Equal(t, tc.expectedDeferredResponses, deferredIncrementalData) diff --git a/graphql/handler/transport/http_multipart_mixed.go b/graphql/handler/transport/http_multipart_mixed.go index c3ac48f9eba..9cf1b533930 100644 --- a/graphql/handler/transport/http_multipart_mixed.go +++ b/graphql/handler/transport/http_multipart_mixed.go @@ -280,7 +280,6 @@ func (a *multipartResponseAggregator) flush(w http.ResponseWriter) { // TODO: use the "HasNext" status of deferResponses items to determine // the operation status and pending / complete fields, but remove from // the incremental (deferResponses) object. - var hasNext bool hasNext = a.deferResponses[len(a.deferResponses)-1].HasNext != nil && *a.deferResponses[len(a.deferResponses)-1].HasNext writeIncrementalJson(w, a.deferResponses, hasNext) From 6bbdd0b0a770fab5fb386b8db96da60860612e6a Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Wed, 27 Nov 2024 13:16:41 -0500 Subject: [PATCH 06/11] add spread fragments --- codegen/testserver/singlefile/defer_test.go | 81 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index ee80533d6ea..4ba8af2f4e3 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -150,7 +150,7 @@ func TestDefer(t *testing.T) { }, }, { - name: "defer single using fragment type", + name: "defer single using inline fragment with type", query: `query testDefer { deferSingle { id @@ -185,6 +185,45 @@ func TestDefer(t *testing.T) { }, }, }, + { + name: "defer single using spread fragment", + query: `query testDefer { + deferSingle { + id + name + ... DeferFragment @defer + } +} + +fragment DeferFragment on DeferModel { + values +} +`, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedDeferredResponses: []deferredData{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Path: []any{"deferSingle"}, + }, + }, + }, { name: "defer single with label", query: `query testDefer { @@ -222,6 +261,46 @@ func TestDefer(t *testing.T) { }, }, }, + { + name: "defer single using spread fragment with label", + query: `query testDefer { + deferSingle { + id + name + ... DeferFragment @defer(label: "test label") + } +} + +fragment DeferFragment on DeferModel { + values +} +`, + expectedInitialResponse: response[struct { + DeferSingle deferModel + }]{ + Data: struct { + DeferSingle deferModel + }{ + DeferSingle: deferModel{ + Id: "1", + Name: "Defer test 1", + Values: nil, + }, + }, + HasNext: true, + }, + expectedDeferredResponses: []deferredData{ + { + Data: struct { + Values []string `json:"values"` + }{ + Values: []string{"test defer 1", "test defer 2", "test defer 3"}, + }, + Label: "test label", + Path: []any{"deferSingle"}, + }, + }, + }, { name: "defer single when if arg is true", query: `query testDefer { From a420aa65899516da0d01033882932c7309f6d48c Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Wed, 27 Nov 2024 13:22:56 -0500 Subject: [PATCH 07/11] lint --- client/incremental_http.go | 11 +++++++---- codegen/testserver/singlefile/defer_test.go | 16 ++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/client/incremental_http.go b/client/incremental_http.go index c02f2569836..c1c507c6ba3 100644 --- a/client/incremental_http.go +++ b/client/incremental_http.go @@ -3,6 +3,7 @@ package client import ( "context" "encoding/json" + "errors" "fmt" "io" "mime" @@ -93,7 +94,7 @@ func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...O w := httptest.NewRecorder() p.h.ServeHTTP(w, r) - res := w.Result() + res := w.Result() //nolint:bodyclose // Remains open since we are reading from it incrementally. if res.StatusCode >= http.StatusBadRequest { return errorIncremental(fmt.Errorf("http %d: %s", w.Code, w.Body.String())) } @@ -110,12 +111,12 @@ func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...O // expected range. deferSpec, ok := params["deferspec"] if !ok || deferSpec == "" { - return errorIncremental(fmt.Errorf("expected deferSpec in content-type")) + return errorIncremental(errors.New("expected deferSpec in content-type")) } boundary, ok := params["boundary"] if !ok || boundary == "" { - return errorIncremental(fmt.Errorf("expected boundary in content-type")) + return errorIncremental(errors.New("expected boundary in content-type")) } mr := multipart.NewReader(res.Body, boundary) @@ -130,6 +131,7 @@ func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...O next: func(response any) (err error) { defer func() { if err != nil { + res.Body.Close() cancel(err) } }() @@ -157,6 +159,7 @@ func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...O } if next.Err == io.EOF { + res.Body.Close() cancel(context.Canceled) return nil } @@ -181,7 +184,7 @@ func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...O // We want to unpack even if there is an error, so we can see partial // responses. err = unpack(data, response, p.dc) - if rawErrors != nil { + if len(rawErrors) != 0 { err = RawJsonError{rawErrors} return err } diff --git a/codegen/testserver/singlefile/defer_test.go b/codegen/testserver/singlefile/defer_test.go index 4ba8af2f4e3..b0c56e86056 100644 --- a/codegen/testserver/singlefile/defer_test.go +++ b/codegen/testserver/singlefile/defer_test.go @@ -110,7 +110,7 @@ func TestDefer(t *testing.T) { cases := []struct { name string query string - expectedInitialResponse interface{} + expectedInitialResponse any expectedDeferredResponses []deferredData }{ { @@ -491,14 +491,14 @@ fragment DeferFragment on DeferModel { if !valueResp.HasNext { deferredResponses = append(deferredResponses, valueResp) break - } else { - // Remove HasNext from comparison: we don't know the order they will be - // delivered in, and so this can't be known in the setup. But if HasNext - // does not work right we will either error out or get too few - // responses, so it's still checked. - valueResp.HasNext = false - deferredResponses = append(deferredResponses, valueResp) } + + // Remove HasNext from comparison: we don't know the order they will be + // delivered in, and so this can't be known in the setup. But if HasNext + // does not work right we will either error out or get too few + // responses, so it's still checked. + valueResp.HasNext = false + deferredResponses = append(deferredResponses, valueResp) } require.NoError(t, read.Close()) From 7dceb1c901e22abb60fc5047da1bb1c2e8526f3e Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Wed, 27 Nov 2024 13:41:39 -0500 Subject: [PATCH 08/11] close body --- client/incremental_http.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/incremental_http.go b/client/incremental_http.go index c1c507c6ba3..bd2c8dd1c45 100644 --- a/client/incremental_http.go +++ b/client/incremental_http.go @@ -125,6 +125,7 @@ func (p *Client) IncrementalHTTP(ctx context.Context, query string, options ...O return &IncrementalHandler{ close: func() error { + res.Body.Close() cancel(context.Canceled) return nil }, From e4dff3861d31e94def6294b8f11bd416084a6084 Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Mon, 2 Dec 2024 09:59:54 -0500 Subject: [PATCH 09/11] update followschema generated test server to match --- codegen/testserver/followschema/defer.graphql | 4 +-- codegen/testserver/followschema/resolver.go | 8 +++--- .../followschema/root_.generated.go | 16 +++++------ .../followschema/schema.generated.go | 28 +++++++++---------- codegen/testserver/followschema/stub.go | 12 ++++---- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/codegen/testserver/followschema/defer.graphql b/codegen/testserver/followschema/defer.graphql index c31e0def87a..3d20ea61bcc 100644 --- a/codegen/testserver/followschema/defer.graphql +++ b/codegen/testserver/followschema/defer.graphql @@ -1,6 +1,6 @@ extend type Query { - deferCase1: DeferModel - deferCase2: [DeferModel!] + deferSingle: DeferModel + deferMultiple: [DeferModel!] } type DeferModel { diff --git a/codegen/testserver/followschema/resolver.go b/codegen/testserver/followschema/resolver.go index 500053e6429..2b5f8f6fbbf 100644 --- a/codegen/testserver/followschema/resolver.go +++ b/codegen/testserver/followschema/resolver.go @@ -197,13 +197,13 @@ func (r *queryResolver) DefaultParameters(ctx context.Context, falsyBoolean *boo panic("not implemented") } -// DeferCase1 is the resolver for the deferCase1 field. -func (r *queryResolver) DeferCase1(ctx context.Context) (*DeferModel, error) { +// DeferSingle is the resolver for the deferSingle field. +func (r *queryResolver) DeferSingle(ctx context.Context) (*DeferModel, error) { panic("not implemented") } -// DeferCase2 is the resolver for the deferCase2 field. -func (r *queryResolver) DeferCase2(ctx context.Context) ([]*DeferModel, error) { +// DeferMultiple is the resolver for the deferMultiple field. +func (r *queryResolver) DeferMultiple(ctx context.Context) ([]*DeferModel, error) { panic("not implemented") } diff --git a/codegen/testserver/followschema/root_.generated.go b/codegen/testserver/followschema/root_.generated.go index deb06db4a61..b863f1a64b7 100644 --- a/codegen/testserver/followschema/root_.generated.go +++ b/codegen/testserver/followschema/root_.generated.go @@ -326,8 +326,8 @@ type ComplexityRoot struct { Collision func(childComplexity int) int DefaultParameters func(childComplexity int, falsyBoolean *bool, truthyBoolean *bool) int DefaultScalar func(childComplexity int, arg string) int - DeferCase1 func(childComplexity int) int - DeferCase2 func(childComplexity int) int + DeferMultiple func(childComplexity int) int + DeferSingle func(childComplexity int) int DeprecatedField func(childComplexity int) int DirectiveArg func(childComplexity int, arg string) int DirectiveDouble func(childComplexity int) int @@ -1281,19 +1281,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.DefaultScalar(childComplexity, args["arg"].(string)), true - case "Query.deferCase1": - if e.complexity.Query.DeferCase1 == nil { + case "Query.deferMultiple": + if e.complexity.Query.DeferMultiple == nil { break } - return e.complexity.Query.DeferCase1(childComplexity), true + return e.complexity.Query.DeferMultiple(childComplexity), true - case "Query.deferCase2": - if e.complexity.Query.DeferCase2 == nil { + case "Query.deferSingle": + if e.complexity.Query.DeferSingle == nil { break } - return e.complexity.Query.DeferCase2(childComplexity), true + return e.complexity.Query.DeferSingle(childComplexity), true case "Query.deprecatedField": if e.complexity.Query.DeprecatedField == nil { diff --git a/codegen/testserver/followschema/schema.generated.go b/codegen/testserver/followschema/schema.generated.go index e98ff8cdcd9..89e58f7c398 100644 --- a/codegen/testserver/followschema/schema.generated.go +++ b/codegen/testserver/followschema/schema.generated.go @@ -49,8 +49,8 @@ type QueryResolver interface { DeprecatedField(ctx context.Context) (string, error) Overlapping(ctx context.Context) (*OverlappingFields, error) DefaultParameters(ctx context.Context, falsyBoolean *bool, truthyBoolean *bool) (*DefaultParametersMirror, error) - DeferCase1(ctx context.Context) (*DeferModel, error) - DeferCase2(ctx context.Context) ([]*DeferModel, error) + DeferSingle(ctx context.Context) (*DeferModel, error) + DeferMultiple(ctx context.Context) ([]*DeferModel, error) DirectiveArg(ctx context.Context, arg string) (*string, error) DirectiveNullableArg(ctx context.Context, arg *int, arg2 *int, arg3 *string) (*string, error) DirectiveSingleNullableArg(ctx context.Context, arg1 *string) (*string, error) @@ -3004,8 +3004,8 @@ func (ec *executionContext) fieldContext_Query_defaultParameters(ctx context.Con return fc, nil } -func (ec *executionContext) _Query_deferCase1(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_deferCase1(ctx, field) +func (ec *executionContext) _Query_deferSingle(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_deferSingle(ctx, field) if err != nil { return graphql.Null } @@ -3018,7 +3018,7 @@ func (ec *executionContext) _Query_deferCase1(ctx context.Context, field graphql }() resTmp := ec._fieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DeferCase1(rctx) + return ec.resolvers.Query().DeferSingle(rctx) }) if resTmp == nil { @@ -3029,7 +3029,7 @@ func (ec *executionContext) _Query_deferCase1(ctx context.Context, field graphql return ec.marshalODeferModel2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋcodegenᚋtestserverᚋfollowschemaᚐDeferModel(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_deferCase1(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Query_deferSingle(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, @@ -3050,8 +3050,8 @@ func (ec *executionContext) fieldContext_Query_deferCase1(_ context.Context, fie return fc, nil } -func (ec *executionContext) _Query_deferCase2(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_deferCase2(ctx, field) +func (ec *executionContext) _Query_deferMultiple(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_deferMultiple(ctx, field) if err != nil { return graphql.Null } @@ -3064,7 +3064,7 @@ func (ec *executionContext) _Query_deferCase2(ctx context.Context, field graphql }() resTmp := ec._fieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DeferCase2(rctx) + return ec.resolvers.Query().DeferMultiple(rctx) }) if resTmp == nil { @@ -3075,7 +3075,7 @@ func (ec *executionContext) _Query_deferCase2(ctx context.Context, field graphql return ec.marshalODeferModel2ᚕᚖgithubᚗcomᚋ99designsᚋgqlgenᚋcodegenᚋtestserverᚋfollowschemaᚐDeferModelᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_deferCase2(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Query_deferMultiple(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, @@ -7627,7 +7627,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "deferCase1": + case "deferSingle": field := field innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { @@ -7636,7 +7636,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_deferCase1(ctx, field) + res = ec._Query_deferSingle(ctx, field) return res } @@ -7646,7 +7646,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) - case "deferCase2": + case "deferMultiple": field := field innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { @@ -7655,7 +7655,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_deferCase2(ctx, field) + res = ec._Query_deferMultiple(ctx, field) return res } diff --git a/codegen/testserver/followschema/stub.go b/codegen/testserver/followschema/stub.go index ee9e7c245de..be8799cae36 100644 --- a/codegen/testserver/followschema/stub.go +++ b/codegen/testserver/followschema/stub.go @@ -71,8 +71,8 @@ type Stub struct { DeprecatedField func(ctx context.Context) (string, error) Overlapping func(ctx context.Context) (*OverlappingFields, error) DefaultParameters func(ctx context.Context, falsyBoolean *bool, truthyBoolean *bool) (*DefaultParametersMirror, error) - DeferCase1 func(ctx context.Context) (*DeferModel, error) - DeferCase2 func(ctx context.Context) ([]*DeferModel, error) + DeferSingle func(ctx context.Context) (*DeferModel, error) + DeferMultiple func(ctx context.Context) ([]*DeferModel, error) DirectiveArg func(ctx context.Context, arg string) (*string, error) DirectiveNullableArg func(ctx context.Context, arg *int, arg2 *int, arg3 *string) (*string, error) DirectiveSingleNullableArg func(ctx context.Context, arg1 *string) (*string, error) @@ -352,11 +352,11 @@ func (r *stubQuery) Overlapping(ctx context.Context) (*OverlappingFields, error) func (r *stubQuery) DefaultParameters(ctx context.Context, falsyBoolean *bool, truthyBoolean *bool) (*DefaultParametersMirror, error) { return r.QueryResolver.DefaultParameters(ctx, falsyBoolean, truthyBoolean) } -func (r *stubQuery) DeferCase1(ctx context.Context) (*DeferModel, error) { - return r.QueryResolver.DeferCase1(ctx) +func (r *stubQuery) DeferSingle(ctx context.Context) (*DeferModel, error) { + return r.QueryResolver.DeferSingle(ctx) } -func (r *stubQuery) DeferCase2(ctx context.Context) ([]*DeferModel, error) { - return r.QueryResolver.DeferCase2(ctx) +func (r *stubQuery) DeferMultiple(ctx context.Context) ([]*DeferModel, error) { + return r.QueryResolver.DeferMultiple(ctx) } func (r *stubQuery) DirectiveArg(ctx context.Context, arg string) (*string, error) { return r.QueryResolver.DirectiveArg(ctx, arg) From e02f94d9fdb132e7538d4345fb8a4ffe306b4f21 Mon Sep 17 00:00:00 2001 From: Steve Coffman Date: Mon, 2 Dec 2024 11:09:10 -0500 Subject: [PATCH 10/11] Update client/incremental_http.go comment wording --- client/incremental_http.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/incremental_http.go b/client/incremental_http.go index bd2c8dd1c45..9128a8f5b21 100644 --- a/client/incremental_http.go +++ b/client/incremental_http.go @@ -68,8 +68,8 @@ func errorIncremental(err error) *IncrementalHandler { // IncrementalHTTP returns a GraphQL response handler for the current // GQLGen implementation of the [incremental delivery over HTTP spec]. -// This spec provides for "streaming" responses triggered by the use of -// @stream or @defer using is an alternate approach to SSE. To that end, +// The IncrementalHTTP spec provides for "streaming" responses triggered by +// the use of @stream or @defer as an alternate approach to SSE. To that end, // the client retains the interface of the handler returned from // Client.SSE. // From 35b0395f35a39bf3ec3ad9c17cf8873e896e6b11 Mon Sep 17 00:00:00 2001 From: Philip Hughes Date: Mon, 2 Dec 2024 11:17:19 -0500 Subject: [PATCH 11/11] lint --- client/incremental_http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/incremental_http.go b/client/incremental_http.go index 9128a8f5b21..d1c2507d2ee 100644 --- a/client/incremental_http.go +++ b/client/incremental_http.go @@ -69,7 +69,7 @@ func errorIncremental(err error) *IncrementalHandler { // IncrementalHTTP returns a GraphQL response handler for the current // GQLGen implementation of the [incremental delivery over HTTP spec]. // The IncrementalHTTP spec provides for "streaming" responses triggered by -// the use of @stream or @defer as an alternate approach to SSE. To that end, +// the use of @stream or @defer as an alternate approach to SSE. To that end, // the client retains the interface of the handler returned from // Client.SSE. //