Skip to content

Instantly share code, notes, and snippets.

@knaeckeKami
Created October 26, 2022 16:12
Show Gist options
  • Select an option

  • Save knaeckeKami/cea6f62971ce7210f9708d7ea2070055 to your computer and use it in GitHub Desktop.

Select an option

Save knaeckeKami/cea6f62971ce7210f9708d7ea2070055 to your computer and use it in GitHub Desktop.
import 'package:gql_exec/gql_exec.dart';
import "package:async/async.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;
import 'package:gql_dio_link/gql_dio_link.dart';
/// 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];
}
// 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;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment