Skip to content

Adopt pending/incremental/completed delivery format - #609

Merged
xperiandri merged 19 commits into
devfrom
incremental-delivery-v02
Sep 21, 2026
Merged

xperiandri merged 19 commits into
devfrom
incremental-delivery-v02

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Updates incremental delivery to the pending / incremental / completed / hasNext wire format.

Review scope

Please review only the incremental-delivery wire format and behavior introduced on top of #608. Response-data contracts, generic streaming/middleware behavior, and WebSocket lifecycle are reviewed separately in #611, #612, and #608 and should not be reviewed again here.

Dependency chain: #611 → #612 → #608 → this PR.

Verification

Focused incremental/WebSocket tests passed (85 passed, 1 skipped). git diff --check is clean.

@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch from 60987fd to 3ec49f7 Compare September 18, 2026 14:05
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from 09a9e82 to 745cd7b Compare September 18, 2026 14:07
@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch from 3ec49f7 to 765ac78 Compare September 18, 2026 16:47
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from 745cd7b to ef14e43 Compare September 18, 2026 17:00
@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch from 765ac78 to 73d00c3 Compare September 18, 2026 17:48
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from ef14e43 to 5dee67c Compare September 18, 2026 17:56
@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch from 73d00c3 to 500d88a Compare September 18, 2026 19:54
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from 5dee67c to 6b2159f Compare September 18, 2026 19:54
@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch from 500d88a to f0ad702 Compare September 18, 2026 23:26
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from 6b2159f to 24fd2f2 Compare September 18, 2026 23:26
@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch from f0ad702 to 19b5f35 Compare September 18, 2026 23:53
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from 24fd2f2 to 248c693 Compare September 18, 2026 23:54
@xperiandri
xperiandri force-pushed the task-seq-field-websocket branch 2 times, most recently from 3e3dbb6 to 4faa338 Compare September 19, 2026 00:00
Base automatically changed from task-seq-field-websocket to dev September 19, 2026 01:45
@xperiandri
xperiandri force-pushed the incremental-delivery-v02 branch from 248c693 to fcad4f4 Compare September 19, 2026 13:57
@xperiandri
xperiandri requested a balanced review from Copilot September 19, 2026 14:00
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Test Results

    9 files      9 suites   14m 58s ⏱️
  860 tests   851 ✅  9 💤 0 ❌
2 580 runs  2 553 ✅ 27 💤 0 ❌

Results for commit b9d7c91.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Lazy stream announcements break Apollo merging, while early failures and empty streams produce incorrect completion behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Adopts the GraphQL 17 incremental-delivery wire format for WebSocket responses.

Changes:

  • Adds incremental result contracts and stateful delivery translation.
  • Emits completion markers for deferred and streamed fields.
  • Updates serialization, behavior tests, and documentation.
File Description
RELEASE_NOTES.md Documents the new wire format.
docs/​type-system.md Documents WebSocket incremental delivery.
src/​FSharp.Data.GraphQL.Shared/​WebSockets.fs Defines incremental payload contracts.
src/​FSharp.Data.GraphQL.Server/​IO.fs Adds deferred completion events.
src/​FSharp.Data.GraphQL.Server/​Execution.fs Emits completion markers.
src/​FSharp.Data.GraphQL.Server.AspNetCore/​IncrementalDelivery.fs Translates engine events into wire payloads.
src/​FSharp.Data.GraphQL.Server.AspNetCore/​GraphQLWebsocketMiddleware.fs Integrates incremental translation.
src/​FSharp.Data.GraphQL.Server.AspNetCore/​GraphQLRequestHandler.fs Logs completion events.
src/​FSharp.Data.GraphQL.Server.AspNetCore/​FSharp.Data.GraphQL.Server.AspNetCore.fsproj Includes the translator source.
tests/​FSharp.Data.GraphQL.Tests/​Helpers.fs Adds completion-filtering helper.
tests/​FSharp.Data.GraphQL.Tests/​DeferredTests.fs Adapts deferred tests.
tests/​FSharp.Data.GraphQL.Tests/​TaskSeqFieldTests.fs Tests stream completion behavior.
tests/​FSharp.Data.GraphQL.Tests/​MiddlewareTests.fs Adapts middleware assertions.
tests/​FSharp.Data.GraphQL.Tests/​Relay/​NodeTests.fs Adapts Relay assertions.
tests/​FSharp.Data.GraphQL.Tests/​AspNetCore/​SerializationTests.fs Tests new payload serialization.
tests/​FSharp.Data.GraphQL.Tests/​AspNetCore/​IncrementalDeliveryTests.fs Tests delivery translation and ordering.
tests/​FSharp.Data.GraphQL.Tests/​AspNetCore/​IncrementalPayloadSplittingTests.fs Removes obsolete splitting tests.
tests/​FSharp.Data.GraphQL.Tests/​FSharp.Data.GraphQL.Tests.fsproj Registers replacement tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs Outdated
Comment thread src/FSharp.Data.GraphQL.Server.AspNetCore/IncrementalDelivery.fs Outdated
Comment thread src/FSharp.Data.GraphQL.Server.AspNetCore/IncrementalDelivery.fs
Comment thread src/FSharp.Data.GraphQL.Server.AspNetCore/IncrementalDelivery.fs Outdated
@xperiandri

Copy link
Copy Markdown
Collaborator Author

@copilot descrive GQLDeferredResponseContent cases with XML comments

Copilot AI commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

@copilot descrive GQLDeferredResponseContent cases with XML comments

Addressed in 1f96f19.

Comment thread src/FSharp.Data.GraphQL.Server/IO.fs
Comment thread tests/FSharp.Data.GraphQL.Tests/Helpers.fs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Variable-based defer labels are currently dropped, and existing struct-nullable TaskSeq coverage was removed.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Low severity Restore namespace ordering with System namespaces first

tests/​FSharp.Data.GraphQL.Tests/​TaskSeqFieldTests.fs:11

The project’s open-order convention puts System namespaces first; moving Xunit above them makes this file inconsistent. Restore the System/FSharp/external ordering.

Low severity Preserve StructNullable list defer coverage in a separate test

tests/​FSharp.Data.GraphQL.Tests/​TaskSeqFieldTests.fs:197

This repurposes the only TaskSeq defer test that covered StructNullable (ListOf ...), leaving that supported wrapper combination untested. Keep the struct-nullable test and add the completion-order assertion as a separate fact (or retain the original field definition and initial-data assertion here).

@xperiandri

Copy link
Copy Markdown
Collaborator Author

tests/​FSharp.Data.GraphQL.Tests/​TaskSeqFieldTests.fs:197

This repurposes the only TaskSeq defer test that covered StructNullable (ListOf ...), leaving that supported wrapper combination untested. Keep the struct-nullable test and add the completion-order assertion as a separate fact (or retain the original field definition and initial-data assertion here).

@copilot fix

Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>

Copilot AI commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

tests/​FSharp.Data.GraphQL.Tests/​TaskSeqFieldTests.fs:197

This repurposes the only TaskSeq defer test that covered StructNullable (ListOf ...), leaving that supported wrapper combina...

Fixed in c9cf0e8. The original defer test now again covers StructNullable (ListOf ...), and the completion-order assertion remains in its own separate fact.

xperiandri and others added 5 commits September 21, 2026 01:12
Engine, translator and serialization tests for what the branch already
delivers (end-to-end `pending`/`incremental`/`completed`/`hasNext`
sequences with protocol invariants, defer inside stream, error bubbling
inside a deferred payload, null parents, root-level and mutation defers),
plus skipped tests for every spec feature not implemented yet (`if`,
`initialCount`, stream `label`, fragment-level defer, `subPath`, validation
rules) and three real-socket tests of the `graphql-transport-ws` middleware
hosted through `WebApplicationFactory`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each connection now runs a reader loop (sole reader of the socket), a
control loop (sole owner of the subscription registry) and a sender loop
(sole writer of the socket, including its close), plus one worker per
subscription whose observer only queues events. Every producer writes into
a `System.Threading.Channels` channel, so no lock, no `SemaphoreSlim` gate,
no `.Wait()` and no start-up barrier remain; the connection-init timeout is
a `Task.WhenAny` instead of a cancelled receive, and shutdown closes the
socket gracefully.

The former closure nest of the middleware is split into WebSocketErrors,
WebSocketMessaging, WebSocketTransport, SubscriptionPayloads,
SubscriptionWorker and WebSocketConnection; `GraphQLSubscriptionsManagement`
and the public `SubscriptionsDict`, `SubscriptionUnsubscriber` and
`OnUnsubscribeAction` aliases are removed, as is the now unused
`Observable.withCompletionMarker`.

Wire contract: `SubscriptionExecutionResult.Data` and `IncrementalResult.Data`
are `Skippable<obj voption>`, `IncrementalResult` gains `SubPath`, a deferred
field is announced at the object containing it and delivered as an object
map of that field, and a field whose announcement never reached the client
is no longer completed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`@stream` only on list fields; no `@defer` or `@stream` in a subscription
operation or on a mutation root field unless disabled with `if: false`;
labels must be string literals, unique in the document.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tal delivery

The announcements of nested deferred and streamed fields are carried as
data by a private observable wrapper (`AnnouncedEvents`), so the containing
payload replays them with a plain concatenation instead of capturing them
under a lock at subscription time; `ResolverResult` keeps its signature.
`ofAsyncEnumerableResolved` delivers results through a single-reader
channel: the emitter is the only caller of the observer and releases each
concurrency slot after emitting, which keeps the throttle semantics of the
`SemaphoreSlim`, the only synchronization primitive left.

`@defer` and `@stream` declare `if: Boolean = true` and `label: String`,
`@stream` also `initialCount: Int = 0`; `@defer` is allowed on fields,
fragment spreads and inline fragments, `@stream` on fields only. `if: false`
executes the field inline (a literal is decided while planning, a variable
at execution), `initialCount` delivers the first items with the initial
payload and streams the rest through the same enumerator, and a stream's
label is carried by its announcement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Initial-count streams can lose remaining items, and unresolved conformance issues affect fragment deferral, label validation, and error serialization.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 3 Medium severity

Open (4)

Comment thread src/FSharp.Data.GraphQL.Server.AspNetCore/IncrementalDelivery.fs
Comment thread src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs
Comment thread src/FSharp.Data.GraphQL.Shared/Validation.fs Outdated
Comment thread src/FSharp.Data.GraphQL.Shared/WebSockets.fs Outdated

@xperiandri xperiandri left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap all directive names in XML comments into and apply

wrapper in that case.
Add XML comments to params of constructors on new types. for example

type internal SubscriptionWorker<'T>
    <para name="id">What is this</para>
    (
        id : SubscriptionId,
        generation : int,
        source : IObservable<'T>,
        payloads : ISubscriptionPayloads<'T>,
        outbound : ChannelWriter<OutboundMessage>,
        inbox : ChannelWriter<ConnectionEvent>,
        logger : ILogger
    ) =

Comment thread src/FSharp.Data.GraphQL.Server.AspNetCore/WebSocketErrors.fs Outdated
Comment thread src/FSharp.Data.GraphQL.Server/Planning.fs
Comment thread src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs Outdated
Comment thread tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs Outdated
Comment thread tests/FSharp.Data.GraphQL.Tests/AspNetCore/SubscriptionWorkerTests.fs Outdated
Comment thread tests/FSharp.Data.GraphQL.Tests/AstValidationTests.fs Outdated
Comment thread RELEASE_NOTES.md Outdated
@xperiandri

Copy link
Copy Markdown
Collaborator Author

Also add links to spec paragraphs that you reference as <see href=

xperiandri and others added 2 commits September 21, 2026 02:00
- `DeferredPending` carries `InitialCount`, so the graphql-transport-ws
  translator expects the streamed items of a `@stream(initialCount: n)`
  field from index n instead of buffering them forever.
- `@defer` and `@stream` labels are unique per operation, over the
  fragments the operation spreads, instead of across the whole document.
- `errors` is omitted from a result payload when there are none, as the
  GraphQL response format requires.
- `GraphQLTransportWS.SubProtocol` names the sub-protocol once.
- Directive names in XML comments are wrapped in `<c>`, the comments
  referring to the specification link to it, and the constructors of the
  new connection types document their parameters.
- Review suggestions applied: `vtryFind`/`vchoose`, open-statement groups,
  the stale test header, release notes grouped by action.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A doc comment that uses any tag must start with `<summary>`, otherwise
the compiler escapes the tags into the text. The project instructions
now also say where the parameters of a primary constructor are
documented.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@xperiandri

Copy link
Copy Markdown
Collaborator Author

Spec links added as <see href> to the directive definitions, the validation rules and the translator in 00eaebb; the XML comments that used a tag without <summary> were wrapped in 72180cf.

xperiandri and others added 2 commits September 21, 2026 02:28
The parameters of a primary constructor are documented on the
constructor itself: the `<param>` lines go between the type name and
the parameter list, not into the type's comment. The project
instructions now show the form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The nested stream test accepts its two items delivered either one by one
or as the one batch the engine produces when both resolve into the same
buffered event, and the delivery wait allows for a slow test item taking
several times its sleep on a busy CI runner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The broad public-contract, execution, incremental translation, and concurrent WebSocket lifecycle changes require final human validation despite substantial focused coverage.

Review effort: Balanced
Findings: None

Resolved since last review (4)

@xperiandri
xperiandri merged commit c0ba6d4 into dev Sep 21, 2026
6 checks passed
@xperiandri
xperiandri deleted the incremental-delivery-v02 branch September 21, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants