Skip to content

close reason should be optional and options.shouldRetry -> bool as a replacement for isFatalConnectionProblem #418

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

Merged
merged 5 commits into from
Nov 11, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 50 additions & 21 deletions links/gql_websocket_link/lib/src/graphql_transport_ws.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ class TransportWsEvent {
received = null,
message = null,
event = null;

const TransportWsEvent.opened(WebSocketChannel this.socket)
: type = TransportWsEventType.opened,
payload = null,
received = null,
message = null,
event = null;

const TransportWsEvent.connected(
WebSocketChannel this.socket,
this.payload,
Expand All @@ -116,6 +118,7 @@ class TransportWsEvent {
socket = null,
message = null,
event = null;

const TransportWsEvent.pong(
this.payload, {
required bool this.received,
Expand All @@ -130,12 +133,14 @@ class TransportWsEvent {
received = null,
socket = null,
event = null;

const TransportWsEvent.closed(Object this.event)
: type = TransportWsEventType.closed,
payload = null,
received = null,
message = null,
socket = null;

const TransportWsEvent.error(Object this.event)
: type = TransportWsEventType.error,
payload = null,
Expand Down Expand Up @@ -382,6 +387,17 @@ class TransportWsClientOptions {
/// @default [randomizedExponentialBackoff]
final Future<void> Function(int retries) retryWait;

/// Check if the close event or connection error is fatal. If you return `false`,
/// the client will fail immediately without additional retries; however, if you
/// return `true`, the client will keep retrying until the `retryAttempts` have
/// been exceeded.
/// The argument is whatever has been thrown during the connection phase.
/// Beware, the library classifies a few close events as fatal regardless of
/// what is returned here. They are listed in the documentation of the
/// `retryAttempts` option.
/// @default [shouldRetryDefault]
final bool Function(Object errOrCloseEvent) shouldRetry;

/// Check if the close event or connection error is fatal. If you return `true`,
/// the client will fail immediately without additional retries; however, if you
/// return `false`, the client will keep retrying until the `retryAttempts` have
Expand Down Expand Up @@ -464,6 +480,7 @@ class TransportWsClientOptions {
this.disablePong = false,
this.retryAttempts = 0,
this.retryWait = randomizedExponentialBackoff,
this.shouldRetry = shouldRetryDefault,
this.isFatalConnectionProblem = isFatalConnectionProblemDefault,
this.eventHandlers,
this.generateID = generateUUID,
Expand Down Expand Up @@ -506,6 +523,29 @@ class TransportWsClientOptions {
final v = c.group(0) == "x" ? r : (r & 0x3) | 0x8;
return v.toRadixString(16);
});

/// By default, connection should not retry on fatal errors
static bool shouldRetryDefault(Object errOrCloseEvent) {
if (errOrCloseEvent is LikeCloseEvent &&
(_isFatalInternalCloseCode(errOrCloseEvent.code) ||
const [
CloseCode.internalServerError,
CloseCode.internalClientError,
CloseCode.badRequest,
CloseCode.badResponse,
CloseCode.unauthorized,
// CloseCode.Forbidden, might grant access out after retry
CloseCode.subprotocolNotAcceptable,
// CloseCode.ConnectionInitialisationTimeout, might not time out after retry
// CloseCode.ConnectionAcknowledgementTimeout, might not time out after retry
CloseCode.subscriberAlreadyExists,
CloseCode.tooManyInitialisationRequests,
// 4499, // Terminated, probably because the socket froze, we want to retry
].contains(errOrCloseEvent.code))) {
return false;
}
return true;
}
}

class _Connected {
Expand Down Expand Up @@ -588,29 +628,15 @@ class _ConnectionState {
/// Checks the `connect` problem and evaluates if the client should retry.
bool shouldRetryConnectOrThrow(Object errOrCloseEvent) {
options.log?.call("shouldRetryConnectOrThrow $errOrCloseEvent");
// some close codes are worth reporting immediately
if (errOrCloseEvent is LikeCloseEvent &&
(_isFatalInternalCloseCode(errOrCloseEvent.code) ||
const [
CloseCode.internalServerError,
CloseCode.internalClientError,
CloseCode.badRequest,
CloseCode.badResponse,
CloseCode.unauthorized,
// CloseCode.Forbidden, might grant access out after retry
CloseCode.subprotocolNotAcceptable,
// CloseCode.ConnectionInitialisationTimeout, might not time out after retry
// CloseCode.ConnectionAcknowledgementTimeout, might not time out after retry
CloseCode.subscriberAlreadyExists,
CloseCode.tooManyInitialisationRequests,
// 4499, // Terminated, probably because the socket froze, we want to retry
].contains(errOrCloseEvent.code))) {
throw errOrCloseEvent;
}

// client was disposed, no retries should proceed regardless
if (disposed) return false;

// some close codes are worth reporting immediately
if (!options.shouldRetry(errOrCloseEvent)) {
throw errOrCloseEvent;
}

// normal closure (possibly all subscriptions have completed)
// if no locks were acquired in the meantime, shouldnt try again
if (errOrCloseEvent is LikeCloseEvent && errOrCloseEvent.code == 1000) {
Expand Down Expand Up @@ -850,7 +876,7 @@ class _ConnectionState {
? "DONE"
: LikeCloseEvent(
code: socket.closeCode!,
reason: socket.closeReason!,
reason: socket.closeReason,
),
),
onError: (Object err) => onError?.call(err),
Expand Down Expand Up @@ -929,7 +955,9 @@ class _Client extends TransportWsClient {
_Client({required this.state});

final _ConnectionState state;

_Emitter get emitter => state.emitter;

@override
TransportWsClientOptions get options => state.options;

Expand Down Expand Up @@ -1066,6 +1094,7 @@ class _Emitter {
void Function(TransportWsMessage) listener,
) onMessage;
final void Function(String logMessage)? log;

_Emitter({
required this.listeners,
required this.onMessage,
Expand Down Expand Up @@ -1153,7 +1182,7 @@ class LikeCloseEvent {
final int code;

/// Returns the WebSocket connection close reason provided by the server. */
final String reason;
final String? reason;

final bool? wasClean;

Expand Down