-
-
Notifications
You must be signed in to change notification settings - Fork 643
Feature/add cancellation support #1510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lewinpauli
wants to merge
4
commits into
zino-hofmann:main
Choose a base branch
from
lewinpauli:feature/add-cancellation-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b86110e
feature: add cancellation support for queries and mutations
lewinpauli 12dd5cd
added operation cancellation support to documentation
lewinpauli 03125aa
Added true HTTP request cancellation to the graphql package.
lewinpauli 8a0d116
dart format --line-length=80 ./
lewinpauli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| # Cancellation Support | ||
|
|
||
| The graphql package now supports cancellation of `query` and `mutate` operations. This allows you to cancel in-flight operations when they're no longer needed (e.g., when a user navigates away or cancels an action). | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ### Option 1: Using CancellationToken directly | ||
|
|
||
| ```dart | ||
| import 'package:graphql/client.dart'; | ||
|
|
||
| // Create a cancellation token | ||
| final cancellationToken = CancellationToken(); | ||
|
|
||
| // Execute a query with the cancellation token | ||
| final resultFuture = client.query( | ||
| QueryOptions( | ||
| document: gql(r''' | ||
| query GetUser($id: ID!) { | ||
| user(id: $id) { | ||
| id | ||
| name | ||
| } | ||
| } | ||
| '''), | ||
| variables: {'id': '123'}, | ||
| cancellationToken: cancellationToken, | ||
| ), | ||
| ); | ||
|
|
||
| // Later, if you need to cancel the operation: | ||
| cancellationToken.cancel(); | ||
|
|
||
| // The resultFuture will complete with a QueryResult containing | ||
| // a CancelledException | ||
| try { | ||
| final result = await resultFuture; | ||
| print('Result: ${result.data}'); | ||
| } catch (e) { | ||
| if (e is OperationException && | ||
| e.linkException is CancelledException) { | ||
| print('Operation was cancelled'); | ||
| } | ||
| } | ||
|
|
||
| // Don't forget to dispose the token when done | ||
| cancellationToken.dispose(); | ||
| ``` | ||
|
|
||
| ### Option 2: Using the convenience methods | ||
|
|
||
| The package provides `queryCancellable` and `mutateCancellable` convenience methods that automatically create a `CancellationToken` for you: | ||
|
|
||
| ```dart | ||
| import 'package:graphql/client.dart'; | ||
|
|
||
| // Execute a cancellable query | ||
| final operation = client.queryCancellable( | ||
| QueryOptions( | ||
| document: gql(r''' | ||
| query GetPosts { | ||
| posts { | ||
| id | ||
| title | ||
| content | ||
| } | ||
| } | ||
| '''), | ||
| ), | ||
| ); | ||
|
|
||
| // Access the result future | ||
| final resultFuture = operation.result; | ||
|
|
||
| // Cancel the operation if needed | ||
| operation.cancel(); | ||
|
|
||
| // Handle the result | ||
| try { | ||
| final result = await resultFuture; | ||
| print('Posts: ${result.data}'); | ||
| } catch (e) { | ||
| if (e is OperationException && | ||
| e.linkException is CancelledException) { | ||
| print('Query was cancelled'); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Mutation Example | ||
|
|
||
| ```dart | ||
| import 'package:graphql/client.dart'; | ||
|
|
||
| // Execute a cancellable mutation | ||
| final operation = client.mutateCancellable( | ||
| MutationOptions( | ||
| document: gql(r''' | ||
| mutation CreatePost($title: String!, $content: String!) { | ||
| createPost(title: $title, content: $content) { | ||
| id | ||
| title | ||
| content | ||
| } | ||
| } | ||
| '''), | ||
| variables: { | ||
| 'title': 'New Post', | ||
| 'content': 'This is the content', | ||
| }, | ||
| ), | ||
| ); | ||
|
|
||
| // Cancel if user navigates away | ||
| // operation.cancel(); | ||
|
|
||
| try { | ||
| final result = await operation.result; | ||
| print('Created post: ${result.data}'); | ||
| } catch (e) { | ||
| if (e is OperationException && | ||
| e.linkException is CancelledException) { | ||
| print('Mutation was cancelled'); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Use Cases | ||
|
|
||
| ### 1. User Navigation | ||
| Cancel pending requests when a user navigates away from a page: | ||
|
|
||
| ```dart | ||
| class MyWidget extends StatefulWidget { | ||
| @override | ||
| _MyWidgetState createState() => _MyWidgetState(); | ||
| } | ||
|
|
||
| class _MyWidgetState extends State<MyWidget> { | ||
| CancellableOperation<QueryResult>? _operation; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _operation = client.queryCancellable( | ||
| QueryOptions(document: gql('query { ... }')), | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| // Cancel the operation when the widget is disposed | ||
| _operation?.cancel(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return FutureBuilder( | ||
| future: _operation?.result, | ||
| builder: (context, snapshot) { | ||
| // Build your UI | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 2. Search Debouncing | ||
| Cancel previous search requests when a new one is initiated: | ||
|
|
||
| ```dart | ||
| CancellableOperation<QueryResult>? _searchOperation; | ||
|
|
||
| void search(String query) { | ||
| // Cancel the previous search | ||
| _searchOperation?.cancel(); | ||
|
|
||
| // Start a new search | ||
| _searchOperation = client.queryCancellable( | ||
| QueryOptions( | ||
| document: gql(r''' | ||
| query Search($query: String!) { | ||
| search(query: $query) { | ||
| id | ||
| name | ||
| } | ||
| } | ||
| '''), | ||
| variables: {'query': query}, | ||
| ), | ||
| ); | ||
|
|
||
| _searchOperation!.result.then((result) { | ||
| // Handle search results | ||
| }).catchError((e) { | ||
| if (e is OperationException && | ||
| e.linkException is CancelledException) { | ||
| // Search was cancelled, ignore | ||
| return; | ||
| } | ||
| // Handle other errors | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| ### 3. Timeout with Custom Message | ||
| Combine with timeouts for better control: | ||
|
|
||
| ```dart | ||
| final cancellationToken = CancellationToken(); | ||
|
|
||
| // Set a custom timeout | ||
| Timer(Duration(seconds: 10), () { | ||
| cancellationToken.cancel(); | ||
| }); | ||
|
|
||
| final result = await client.query( | ||
| QueryOptions( | ||
| document: gql('query { ... }'), | ||
| cancellationToken: cancellationToken, | ||
| ), | ||
| ); | ||
| ``` | ||
|
|
||
| ## Important Notes | ||
|
|
||
| 1. **Disposing CancellationTokens**: When using `CancellationToken` directly, remember to call `dispose()` when you're done with it to clean up resources. | ||
|
|
||
| 2. **Convenience Methods**: The `queryCancellable` and `mutateCancellable` methods automatically create and manage `CancellationToken` instances for you. | ||
|
|
||
| 3. **Error Handling**: Cancelled operations will complete with an `OperationException` containing a `CancelledException` as the `linkException`. | ||
|
|
||
| 4. **Network Cleanup**: Cancelling an operation will attempt to cancel the underlying network request, but depending on the transport layer and server, the request may still complete on the server side. | ||
|
|
||
| 5. **Cache Updates**: Cancelled operations will not update the cache with any results they may have received before cancellation. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import 'dart:async'; | ||
|
|
||
| /// A token that can be used to cancel an in-flight GraphQL operation. | ||
| /// | ||
| /// Create a [CancellationToken] and pass it to operations like [GraphQLClient.query] | ||
| /// or [GraphQLClient.mutate] to enable cancellation of those operations. | ||
| /// | ||
| /// {@tool snippet} | ||
| /// Basic usage | ||
| /// | ||
| /// ```dart | ||
| /// final cancellationToken = CancellationToken(); | ||
| /// | ||
| /// // Start a query | ||
| /// final resultFuture = client.query( | ||
| /// QueryOptions( | ||
| /// document: gql('query { ... }'), | ||
| /// cancellationToken: cancellationToken, | ||
| /// ), | ||
| /// ); | ||
| /// | ||
| /// // Later, cancel the operation | ||
| /// cancellationToken.cancel(); | ||
| /// | ||
| /// // The resultFuture will complete with a QueryResult containing | ||
| /// // a CancelledException | ||
| /// ``` | ||
| /// {@end-tool} | ||
| class CancellationToken { | ||
| bool _isCancelled = false; | ||
| final _controller = StreamController<void>.broadcast(); | ||
|
|
||
| /// Whether this token has been cancelled. | ||
| bool get isCancelled => _isCancelled; | ||
|
|
||
| /// A stream that emits when cancellation is requested. | ||
| Stream<void> get onCancel => _controller.stream; | ||
|
|
||
| /// Cancel the operation associated with this token. | ||
| /// | ||
| /// This will cause any in-flight operation to be terminated and complete | ||
| /// with a [CancelledException]. | ||
| /// | ||
| /// Calling [cancel] multiple times has no additional effect. | ||
| void cancel() { | ||
| if (!_isCancelled) { | ||
| _isCancelled = true; | ||
| _controller.add(null); | ||
| } | ||
| } | ||
|
|
||
| /// Dispose of this token's resources. | ||
| /// | ||
| /// After calling [dispose], this token should not be used again. | ||
| void dispose() { | ||
| _controller.close(); | ||
| } | ||
| } | ||
|
|
||
| /// Result wrapper that includes both the result future and a cancellation token. | ||
| /// | ||
| /// This allows you to cancel the operation if needed. | ||
| class CancellableOperation<T> { | ||
| /// The future that will complete with the operation result. | ||
| final Future<T> result; | ||
|
|
||
| /// The cancellation token that can be used to cancel this operation. | ||
| final CancellationToken cancellationToken; | ||
|
|
||
| CancellableOperation({ | ||
| required this.result, | ||
| required this.cancellationToken, | ||
| }); | ||
|
|
||
| /// Cancel this operation. | ||
| /// | ||
| /// This is a convenience method that calls [CancellationToken.cancel]. | ||
| void cancel() { | ||
| cancellationToken.cancel(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't really see how it cancels the underlying request? can you explain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right i haven't noticed that the underlying http request wasn't actually cancelled, only the subscription was cancelled.
I now added true HTTP request cancellation to the graphql package.
Created custom HttpLink with platform-specific cancellation support
Web: Uses XMLHttpRequest.abort() → shows "(canceled)" in Chrome DevTools
IO (Linux/Desktop/Mobile): Uses HttpClient.close(force: true) + request.abort() → terminates TCP connection
Added CancellationContextEntry to pass the token through the Link chain
Added web package dependency (replaces deprecated dart:html)
Added additional tests for http cancellation
Result:
operation.cancel() now actually aborts the underlying HTTP request
Not just ignoring the response — the network connection is terminated
I verified it, in chrome and linux the http request gets actually cancelled