Skip to content

Commit 5c9cac1

Browse files
committed
Simplified GraphQL web socket response definitions and serialization, fixed deferred output
1 parent 2f0ee57 commit 5c9cac1

File tree

10 files changed

+192
-84
lines changed

10 files changed

+192
-84
lines changed

src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<ItemGroup>
1515
<Compile Include="Helpers.fs" />
1616
<Compile Include="GraphQLOptions.fs" />
17+
<Compile Include="GQLResponseExtensions.fs" />
1718
<Compile Include="GraphQLSubscriptionsManagement.fs" />
1819
<Compile Include="GraphQLWebsocketMiddleware.fs" />
1920
<Compile Include="Parser.fs" />
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
[<AutoOpen>]
2+
module internal FSharp.Data.GraphQL.GraphQLWebSocketResponseExtensions
3+
4+
open System
5+
open System.Collections.Generic
6+
open System.Text.Json.Serialization
7+
open FSharp.Data.GraphQL
8+
open FSharp.Data.GraphQL.Extensions
9+
10+
type GQLWebSocketResponse with
11+
12+
static member Direct (data : Output) = {
13+
Data = Include data
14+
Path = Skip
15+
HasNext = Skip
16+
Errors = Skip
17+
Extensions = Skip
18+
}
19+
static member Direct (data : Output, errors) = {
20+
Data = Include data
21+
Path = Skip
22+
HasNext = Skip
23+
Errors = Skippable.ofList errors
24+
Extensions = Skip
25+
}
26+
static member Deferred (data, path) = {
27+
Data = Include data
28+
Path = Include path
29+
HasNext = Skip
30+
Errors = Skip
31+
Extensions = Skip
32+
}
33+
static member Deferred (data, path, hasNext) = {
34+
Data = Include data
35+
Path = Include path
36+
HasNext = Include hasNext
37+
Errors = Skip
38+
Extensions = Skip
39+
}
40+
static member Error (errors) = {
41+
Data = Skip
42+
Path = Skip
43+
HasNext = Skip
44+
Errors = Include errors
45+
Extensions = Skip
46+
}
47+
static member Error (data, errors) = {
48+
Data = data |> ValueOption.ofObj |> Skippable.ofValueOption
49+
Path = Skip
50+
HasNext = Skip
51+
Errors = Include errors
52+
Extensions = Skip
53+
}
54+
static member Error (data : objnull, path, errors) = {
55+
Data = data |> ValueOption.ofObj |> Skippable.ofValueOption
56+
Path = Include path
57+
HasNext = Skip
58+
Errors = Include errors
59+
Extensions = Skip
60+
}

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,6 @@ type GraphQLWebSocketMiddleware<'Root>
3737
let endpointUrl = PathString options.WebsocketOptions.EndpointUrl
3838
let connectionInitTimeout = options.WebsocketOptions.ConnectionInitTimeout
3939

40-
let serializeServerMessage (jsonSerializerOptions : JsonSerializerOptions) (serverMessage : ServerMessage) = task {
41-
let raw =
42-
match serverMessage with
43-
| ConnectionAck -> { Id = ValueNone; Type = "connection_ack"; Payload = ValueNone }
44-
| ServerPing -> { Id = ValueNone; Type = "ping"; Payload = ValueNone }
45-
| ServerPong p -> { Id = ValueNone; Type = "pong"; Payload = p |> ValueOption.map CustomResponse }
46-
| Next (id, payload) -> { Id = ValueSome id; Type = "next"; Payload = ValueSome <| ExecutionResult payload }
47-
| Complete id -> { Id = ValueSome id; Type = "complete"; Payload = ValueNone }
48-
| Error (id, errMsgs) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMsgs }
49-
return JsonSerializer.Serialize (raw, jsonSerializerOptions)
50-
}
51-
5240
static let invalidJsonInClientMessageError =
5341
Result.Error <| InvalidMessage (4400, "Invalid json in client message")
5442

@@ -83,7 +71,7 @@ type GraphQLWebSocketMiddleware<'Root>
8371
let buffer = ArrayPool.Shared.Rent options.ReadBufferSize
8472
try
8573
let completeMessage = new PooledList<byte> ()
86-
let mutable segmentResponse : WebSocketReceiveResult = null
74+
let mutable segmentResponse : WebSocketReceiveResult | null = null
8775
while (not cancellationToken.IsCancellationRequested)
8876
&& socket |> isSocketOpen
8977
&& ((segmentResponse = null)
@@ -111,17 +99,15 @@ type GraphQLWebSocketMiddleware<'Root>
11199
ArrayPool.Shared.Return buffer
112100
}
113101

114-
let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task {
102+
let sendMessageViaSocket (jsonSerializerOptions : JsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task {
115103
if not (socket.State = WebSocketState.Open) then
116104
logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State)
117105
else
118-
// TODO: Allocate string only if a debugger is attached
119-
let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions
120-
let segment = new ArraySegment<byte> (System.Text.Encoding.UTF8.GetBytes (serializedMessage))
121106
if not (socket.State = WebSocketState.Open) then
122107
logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State)
123108
else
124-
do! socket.SendAsync (segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = CancellationToken.None)
109+
let bytes = JsonSerializer.SerializeToUtf8Bytes(message, jsonSerializerOptions)
110+
do! socket.SendAsync (bytes.AsMemory(), WebSocketMessageType.Text, endOfMessage = true, cancellationToken = CancellationToken.None)
125111

126112
logger.LogTrace ("<- Response: {response}", message)
127113
}
@@ -169,27 +155,26 @@ type GraphQLWebSocketMiddleware<'Root>
169155
let sendMsg = sendMessageViaSocket serializerOptions socket
170156
let rcv () = socket |> rcvMsgViaSocket serializerOptions
171157

172-
let sendOutput id (output : SubscriptionExecutionResult) =
158+
let sendOutput id (output : GQLWebSocketResponse) =
173159
sendMsg (Next (id, output))
174160

175161
let sendSubscriptionResponseOutput id subscriptionResult =
176162
match subscriptionResult with
177-
| SubscriptionResult output -> { Data = ValueSome output; Errors = [] } |> sendOutput id
178-
| SubscriptionErrors (output, errors) ->
163+
| SubscriptionResult data -> GQLWebSocketResponse.Direct data |> sendOutput id
164+
| SubscriptionErrors (data, errors) ->
179165
logger.LogWarning ("Subscription errors: {subscriptionErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))))
180-
{ Data = ValueNone; Errors = errors } |> sendOutput id
166+
GQLWebSocketResponse.Error (data, errors) |> sendOutput id
181167

182168
let sendDeferredResponseOutput id deferredResult =
183169
match deferredResult with
184-
| DeferredResult (obj, path) ->
185-
let output = obj :?> Dictionary<string, obj>
186-
{ Data = ValueSome output; Errors = [] } |> sendOutput id
187-
| DeferredErrors (obj, errors, _) ->
170+
| DeferredResult (data, path) ->
171+
GQLWebSocketResponse.Deferred (data, path) |> sendOutput id
172+
| DeferredErrors (data, errors, path) ->
188173
logger.LogWarning (
189174
"Deferred response errors: {deferredErrors}",
190175
(String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}")))
191176
)
192-
{ Data = ValueNone; Errors = errors } |> sendOutput id
177+
GQLWebSocketResponse.Error (data, path, errors) |> sendOutput id
193178

194179
let sendDeferredResultDelayedBy (ct : CancellationToken) (ms : int) id deferredResult : Task = task {
195180
do! Task.Delay (ms, ct)
@@ -202,16 +187,16 @@ type GraphQLWebSocketMiddleware<'Root>
202187
(subscriptions, socket, observableOutput, serializerOptions)
203188
|> addClientSubscription id sendSubscriptionResponseOutput
204189
| Deferred (data, errors, observableOutput) ->
205-
do! { Data = ValueSome data; Errors = [] } |> sendOutput id
190+
do! GQLWebSocketResponse.Direct data |> sendOutput id
206191
if errors.IsEmpty then
207192
(subscriptions, socket, observableOutput, serializerOptions)
208193
|> addClientSubscription id (sendDeferredResultDelayedBy cancellationToken 5000)
209194
else
210195
()
211-
| Direct (data, _) -> do! { Data = ValueSome data; Errors = [] } |> sendOutput id
212-
| RequestError problemDetails ->
213-
logger.LogWarning("Request errors:\n{errors}", problemDetails)
214-
do! { Data = ValueNone; Errors = problemDetails } |> sendOutput id
196+
| Direct (data, errors) -> do! GQLWebSocketResponse.Error (data, errors) |> sendOutput id
197+
| RequestError errors ->
198+
logger.LogWarning("Request errors:\n{errors}", errors)
199+
do! GQLWebSocketResponse.Error errors |> sendOutput id
215200
}
216201

217202
let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) =
@@ -278,7 +263,7 @@ type GraphQLWebSocketMiddleware<'Root>
278263
do! planExecutionResult |> applyPlanExecutionResult id socket
279264
with ex ->
280265
logger.LogError (ex, "Unexpected error during subscription with id '{id}'", id)
281-
do! sendMsg (Error (id, [new Shared.NameValueLookup ([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
266+
do! sendMsg (Error (id, [ GQLProblemDetails.Create ("Unexpected error during subscription", ex) ]))
282267
| ClientComplete id ->
283268
"ClientComplete" |> logMsgWithIdReceived id
284269
subscriptions
@@ -349,8 +334,7 @@ type GraphQLWebSocketMiddleware<'Root>
349334
do! next.Invoke (ctx)
350335
else if ctx.WebSockets.IsWebSocketRequest then
351336
use! socket = ctx.WebSockets.AcceptWebSocketAsync ("graphql-transport-ws")
352-
let! connectionInitResult = socket |> waitForConnectionInitAndRespondToClient
353-
match connectionInitResult with
337+
match! socket |> waitForConnectionInitAndRespondToClient with
354338
| Result.Error errMsg -> logger.LogWarning errMsg
355339
| Ok _ ->
356340
let longRunningCancellationToken =

src/FSharp.Data.GraphQL.Server/Execution.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ let private resolveUnionType possibleTypesFn (uniondef: UnionDef) =
9090
| Some resolveType -> resolveType
9191
| None -> defaultResolveType possibleTypesFn uniondef
9292

93-
let private createFieldContext objdef argDefs ctx (info: ExecutionInfo) (path : FieldPath) = result {
93+
let private createFieldContext objdef argDefs (ctx : ResolveFieldContext) (info: ExecutionInfo) (path : FieldPath) = result {
9494
let fdef = info.Definition
9595
let! args = getArgumentValues argDefs info.Ast.Arguments ctx.Variables
9696
return

src/FSharp.Data.GraphQL.Server/IO.fs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,23 @@ open FSharp.Data.GraphQL
77
open FSharp.Data.GraphQL.Extensions
88
open FSharp.Data.GraphQL.Types
99

10-
type Output = IDictionary<string, obj>
10+
[<AutoOpen>]
11+
module GQLResponseExtensions =
1112

12-
type GQLResponse =
13-
{ DocumentId: int
14-
Data : Output Skippable
15-
Errors : GQLProblemDetails list Skippable }
16-
static member Direct(documentId, data, errors) =
17-
{ DocumentId = documentId
18-
Data = Include data
19-
Errors = Skippable.ofList errors }
20-
static member Stream(documentId) =
21-
{ DocumentId = documentId
22-
Data = Include null
23-
Errors = Skip }
24-
static member RequestError(documentId, errors) =
25-
{ DocumentId = documentId
26-
Data = Skip
27-
Errors = Include errors }
13+
type GQLResponse with
14+
15+
static member Direct (documentId, data, errors) =
16+
{ Data = Include data
17+
Errors = Skippable.ofList errors
18+
Extensions = Skip }
19+
static member Stream (documentId) =
20+
{ Data = Include null
21+
Errors = Skip
22+
Extensions = Skip }
23+
static member RequestError (documentId, errors) =
24+
{ Data = Skip
25+
Errors = Include errors
26+
Extensions = Skip }
2827

2928
type GQLExecutionResult =
3029
{ DocumentId: int

src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
1818
<_Parameter1>FSharp.Data.GraphQL.Server</_Parameter1>
1919
</AssemblyAttribute>
20+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
21+
<_Parameter1>FSharp.Data.GraphQL.Server.AspNetCore</_Parameter1>
22+
</AssemblyAttribute>
2023
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
2124
<_Parameter1>FSharp.Data.GraphQL.Server.Middleware</_Parameter1>
2225
</AssemblyAttribute>
@@ -63,8 +66,9 @@
6366
<Compile Include="ValidationResultCache.fs" />
6467
<Compile Include="Parser.fs" />
6568
<Compile Include="GQLRequest.fs" />
69+
<Compile Include="GQLResponse.fs" />
6670
<Compile Include="WebSockets.fs" />
67-
<Compile Include="Serialization/JsonConverters.fs" />
71+
<None Include="Serialization/JsonConverters.fs" />
6872
<Compile Include="Serialization/JSON.fs" />
6973
</ItemGroup>
7074

src/FSharp.Data.GraphQL.Shared/GQLRequest.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace FSharp.Data.GraphQL.Shared
1+
namespace FSharp.Data.GraphQL
22

33
open System.Collections.Immutable
44
open System.Text.Json
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
namespace FSharp.Data.GraphQL
2+
3+
open System
4+
open System.Collections.Generic
5+
open System.Text.Json.Serialization
6+
open FSharp.Data.GraphQL
7+
open FSharp.Data.GraphQL.Extensions
8+
9+
type Output = IDictionary<string, obj>
10+
11+
type GQLResponse = {
12+
Data : Output Skippable
13+
Errors : GQLProblemDetails list Skippable
14+
Extensions : Output Skippable
15+
}
16+
17+
type GQLWebSocketResponse = {
18+
Data : objnull Skippable
19+
Path : FieldPath Skippable
20+
HasNext : bool Skippable
21+
Errors : GQLProblemDetails list Skippable
22+
Extensions : Output Skippable
23+
} with
24+
25+
static member OfData (data : Output) = {
26+
Data = Include data
27+
Path = Skip
28+
HasNext = Skip
29+
Errors = Skip
30+
Extensions = Skip
31+
}
32+
static member OfData (data : Output, errors) = {
33+
Data = Include data
34+
Path = Skip
35+
HasNext = Skip
36+
Errors = Skippable.ofList errors
37+
Extensions = Skip
38+
}
39+
static member OfDefered (data, path) = {
40+
Data = Include data
41+
Path = Include path
42+
HasNext = Skip
43+
Errors = Skip
44+
Extensions = Skip
45+
}
46+
static member OfDefered (data, path, hasNext) = {
47+
Data = Include data
48+
Path = Include path
49+
HasNext = Include hasNext
50+
Errors = Skip
51+
Extensions = Skip
52+
}
53+
static member OfErrors (errors) = {
54+
Data = Skip
55+
Path = Skip
56+
HasNext = Skip
57+
Errors = Include errors
58+
Extensions = Skip
59+
}
60+
static member OfErrors (data, errors) = {
61+
Data = data |> ValueOption.ofObj |> Skippable.ofValueOption
62+
Path = Skip
63+
HasNext = Skip
64+
Errors = Include errors
65+
Extensions = Skip
66+
}
67+
static member OfErrors (data : objnull, path, errors) = {
68+
Data = data |> ValueOption.ofObj |> Skippable.ofValueOption
69+
Path = Include path
70+
HasNext = Skip
71+
Errors = Include errors
72+
Extensions = Skip
73+
}

src/FSharp.Data.GraphQL.Shared/Serialization/JSON.fs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ let configureSerializerOptions (jsonFSharpOptions: JsonFSharpOptions) (additiona
1616
jsonFSharpOptions.AddToJsonSerializerOptions options
1717

1818
let configureWSSerializerOptions (jsonFSharpOptions: JsonFSharpOptions) (additionalConverters: JsonConverter seq) (options : JsonSerializerOptions) =
19-
let additionalConverters = seq {
20-
yield new ClientMessageConverter () :> JsonConverter
21-
yield new RawServerMessageConverter ()
22-
yield! additionalConverters
23-
}
19+
let jsonFSharpOptions = jsonFSharpOptions.WithSkippableOptionFields (SkippableOptionFields.Always, true)
2420
configureSerializerOptions jsonFSharpOptions additionalConverters options
2521

2622
let defaultJsonFSharpOptions =

0 commit comments

Comments
 (0)