Created
December 14, 2023 10:49
-
-
Save knaeckeKami/7739c97415f73bad2b2465ec7381ad7a to your computer and use it in GitHub Desktop.
This file contains 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
import 'package:ferry/ferry.dart'; | |
import 'package:gql/ast.dart'; | |
import 'package:gql_dio_link/gql_dio_link.dart'; | |
import 'package:gql_exec/gql_exec.dart' as gql_exec; | |
// ignore_for_file: depend_on_referenced_packages | |
typedef ExceptionHandler = Stream<Response>? Function( | |
Request request, | |
NextLink forward, | |
LinkException exception, | |
); | |
/// like [ErrorLink], but with a twist: | |
/// the given [ExceptionHandler] handles [LinkException]s, like [ErrorLink], | |
/// but will also recursively forward errors until the given [ExceptionHandler] either | |
/// returns null or throws an exception (compared to ErrorLink, which can only handle a single error). | |
class RecursiveErrorLink extends Link { | |
final ExceptionHandler onException; | |
const RecursiveErrorLink({ | |
required this.onException, | |
}); | |
@override | |
Stream<Response> request( | |
Request request, [ | |
NextLink? forward, | |
]) async* { | |
assert(forward != null, | |
'RecursiveErrorLink is not a terminating link, therefore it must be given a forward link'); | |
// forward the request the forward and check for errors | |
await for (final result in Result.captureStream(forward!(request))) { | |
if (result.isError) { | |
final error = result.asError!.error; | |
if (error is LinkException) { | |
// here is the recursion -> the [NextLink] of the Exceptionhandler is [this.request] so | |
// errors will be handled by this link again (with a potentially updated request) | |
final stream = onException(request, (r) => this.request(r, forward), error); | |
if (stream != null) { | |
yield* stream; | |
return; | |
} | |
} | |
yield* Stream.error(error); | |
} else { | |
assert(result.isValue); | |
final response = result.asValue!.value; | |
yield response; | |
} | |
} | |
} | |
} | |
/// A handler of Link Exceptions. | |
/// a [Link] that handles [DioLinkTimeoutException]s transparently and retries them | |
/// [RetryOnTimeOutLink.maxRetries] times. | |
class RetryOnTimeOutLink extends RecursiveErrorLink { | |
RetryOnTimeOutLink({ | |
void Function(String)? log, | |
}) : super( | |
onException: (request, forward, exception) => | |
_retryQueriesOnTimeout(request, forward, exception, log), | |
); | |
static const maxRetryCount = 2; | |
// We'll want to handle timeouts ourselves, so we can retry the request | |
static Stream<gql_exec.Response>? _retryQueriesOnTimeout( | |
gql_exec.Request request, | |
NextLink forward, | |
LinkException exception, | |
void Function(String)? log, | |
) { | |
if (exception is DioLinkTimeoutException) { | |
// If we've already retried [maxRetryCount] times, give up | |
if ((request.context.entry<_RequestRetryCount>()?.count ?? 0) >= maxRetryCount) { | |
log?.call('RetryOnTimeOutLink: Giving up after $maxRetryCount retries'); | |
return null; | |
} | |
final bool requestHasOnlyQueries = request.operation.document.definitions.every((definition) { | |
if (definition is OperationDefinitionNode) { | |
// this definition is an operation (query, mutation, subscription) | |
// check if it is a query, because we only retry queries, not mutations | |
return definition.type == OperationType.query; | |
} | |
// currently the other only possible definition is a fragmentDefinition | |
// assert that this is the case, so if this changes, this will throw assert errors | |
// and we are forced to evaluate the code above ;) | |
assert(definition is FragmentDefinitionNode); | |
return true; | |
}); | |
// request contains a mutation or a subscription, we don't retry those | |
if (!requestHasOnlyQueries) { | |
log?.call('RetryOnTimeOutLink: Not retrying request with mutation or subscription'); | |
return null; | |
} | |
// mark the request as retried so we don't retry it more then maxRetryCount times | |
final updatedRequest = request.updateContextEntry<_RequestRetryCount>( | |
(retries) => _RequestRetryCount((retries?.count ?? 0) + 1)); | |
log?.call('RetryOnTimeOutLink: Retrying request ${request.operation.operationName}'); | |
// And try the request again | |
return forward(updatedRequest); | |
} | |
return null; | |
} | |
} | |
/// A [gql_exec.ContextEntry] that keeps track of the number of times a request has been retried | |
class _RequestRetryCount extends gql_exec.ContextEntry { | |
final int count; | |
const _RequestRetryCount(this.count); | |
@override | |
List<Object?> get fieldsForEquality => [count]; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment