Skip to content

Commit d326a30

Browse files
xperiandriCopilotCopilot
authored
Hardened WebSocket streaming and incremental delivery (#608)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent d5cf4e5 commit d326a30

7 files changed

Lines changed: 551 additions & 223 deletions

File tree

‎RELEASE_NOTES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@
288288

289289
* **Breaking Change** Migrated to .NET 10
290290
* **Breaking Change** Made Relay `Edge` a read-only struct
291-
* **Breaking Change** `SubscriptionExecutionResult.Data` is now `obj Skippable`, and the record has new `Path` and `HasNext` fields for incremental delivery
291+
* **Breaking Change** `SubscriptionExecutionResult.Data` is now `obj voption Skippable`, and the record has new `Path` and `HasNext` fields for incremental delivery
292292
* **Breaking Change** `BufferedStreamOptions.Interval` and `BufferedStreamOptions.PreferredBatchSize` are now `int voption`
293293
* **Breaking Change** `ServerMessage.Error` and `ServerRawPayload.ErrorMessages` now carry `GQLProblemDetails list` instead of `NameValueLookup list`, so an `error` message's `payload` is a standard GraphQL error array as the `graphql-transport-ws` protocol requires
294294
* **Breaking Change** A query or mutation whose non-null root field fails during execution now produces a `Direct` (execution) result with `null` data instead of a `RequestError`, which is now only ever produced for a request rejected before execution (validation, planning, variable or inline argument coercion, a middleware, or the executor itself failing); HTTP and `graphql-transport-ws` responses for such a failure now carry `data: null` as the spec requires, instead of omitting `data` entirely. This also changes the public `GQLResponse.Data`, `GQLResponseContent.Direct.Data`, `DeferredErrors.Data`, and `SubscriptionErrors.Data` signatures to use `voption`
Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
module internal FSharp.Data.GraphQL.Server.AspNetCore.GraphQLSubscriptionsManagement
22

3+
open System
4+
35
open FSharp.Data.GraphQL.Shared.WebSockets
46

57
let addSubscription
68
(id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction)
79
(subscriptions : SubscriptionsDict)
810
=
9-
subscriptions.Add (id, (unsubscriber, onUnsubscribe))
11+
lock subscriptions (fun () -> subscriptions.Add (id, (unsubscriber, onUnsubscribe)))
1012

11-
let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = subscriptions.ContainsKey (id)
13+
let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = lock subscriptions (fun () -> subscriptions.ContainsKey (id))
1214

1315
let executeOnUnsubscribeAndDispose (id : SubscriptionId) (subscription : SubscriptionUnsubscriber * OnUnsubscribeAction) =
1416
match subscription with
@@ -19,15 +21,37 @@ let executeOnUnsubscribeAndDispose (id : SubscriptionId) (subscription : Subscri
1921
unsubscriber.Dispose ()
2022

2123
let removeSubscription (id : SubscriptionId) (subscriptions : SubscriptionsDict) =
22-
match subscriptions.TryGetValue id with
23-
| true, sub ->
24-
sub |> executeOnUnsubscribeAndDispose id
25-
subscriptions.Remove (id) |> ignore
26-
| false, _ -> ()
24+
let subscription =
25+
lock subscriptions (fun () ->
26+
match subscriptions.TryGetValue id with
27+
| true, sub ->
28+
subscriptions.Remove (id) |> ignore
29+
ValueSome sub
30+
| false, _ -> ValueNone)
31+
32+
match subscription with
33+
| ValueSome sub -> sub |> executeOnUnsubscribeAndDispose id
34+
| ValueNone -> ()
2735

2836
let removeAllSubscriptions (subscriptions : SubscriptionsDict) =
29-
subscriptions
30-
|> Seq.iter (fun subscription ->
31-
subscription.Value
32-
|> executeOnUnsubscribeAndDispose subscription.Key)
33-
subscriptions.Clear ()
37+
let subscriptionsToDispose =
38+
lock subscriptions (fun () ->
39+
let snapshot =
40+
subscriptions
41+
|> Seq.map (fun subscription -> struct (subscription.Key, subscription.Value))
42+
|> Seq.toArray
43+
44+
subscriptions.Clear ()
45+
snapshot)
46+
47+
let exceptions = ResizeArray ()
48+
49+
subscriptionsToDispose
50+
|> Array.iter (fun struct (id, subscription) ->
51+
try
52+
subscription |> executeOnUnsubscribeAndDispose id
53+
with ex ->
54+
exceptions.Add ex)
55+
56+
if exceptions.Count > 0 then
57+
raise (AggregateException ("One or more subscriptions failed to unsubscribe.", exceptions))

0 commit comments

Comments
 (0)