apolloapollo-clientapollo-link

Apollo-link @client mutation on successful retry


I'm rendering notifications in my React app upon retries caused by network errors. I wish to clear any such notification if/when a connection is reestablished (a successful retry)

I've used apollo-link-retry and used a custom attempts callback to mutate the cache when retry loop begins and when it times out. This works, but the notifications stay on screen when a successful retry occurs, because the callback is not invoked upon successful retry so I have no way to clear the notifications from cache.

I've tried implementing similar logic using apollo-link-error with similar issue. The link only gets called when an error occurred and a successful retry is not an error.

Here's my configuration of apollo-link-retry which "almost" works:

const retryLink = new RetryLink({
  attempts: (count) => {
    let notifyType
    let shouldRetry = true
    if (count === 1) {
      notifyType = 'CONNECTION_RETRY'
      shouldRetry = true
    } else if (count <= 30) {
      shouldRetry = true
    } else {
      notifyType = 'CONNECTION_TIMEOUT'
      shouldRetry = false
    }
    if (notifyType) {
      client.mutate({
        mutation: gql`
          mutation m($notification: Notification!) {
            raiseNotification(notification: $notification) @client
          }
        `,
        variables: {
          notification: { type: notifyType }
        }
      })
    }
    return shouldRetry
  }
})

Maybe I need to implement a custom link to accomplish this? I'm hoping to find a way to leverage the nice retry logic of apollo-link-retry and additionally emit some state to cache as the logic progresses.


Solution

  • I achieved the desired behavior by doing two things:

    Maintain retry count in link context through the attempts function:

    new RetryLink({
      delay: {
        initial: INITIAL_RETRY_DELAY,
        max: MAX_RETRY_DELAY,
        jitter: true
      },
      attempts: (count, operation, error) => {
        if (!error.message || error.message !== 'Failed to fetch') {
          // If error is not related to connection, do not retry
          return false
        }
        operation.setContext(context => ({ ...context, retryCount: count }))
        return (count <= MAX_RETRY_COUNT)
      }
    })
    

    Implement custom link which subscribes to error and completed events further down the link chain and uses the new context field to decide whether notifications should be raised:

    new ApolloLink((operation, forward) => {
      const context = operation.getContext()
      return new Observable(observer => {
        let subscription, hasApplicationError
        try {
          subscription = forward(operation).subscribe({
            next: result => {
              if (result.errors) {
                // Encountered application error (not network related)
                hasApplicationError = true
                notifications.raiseNotification(apolloClient, 'UNEXPECTED_ERROR')
              }
              observer.next(result)
            },
            error: networkError => {
              // Encountered network error
              if (context.retryCount === 1) {
                // Started retrying
                notifications.raiseNotification(apolloClient, 'CONNECTION_RETRY')
              }
              if (context.retryCount === MAX_RETRY_COUNT) {
                // Timed out after retrying
                notifications.raiseNotification(apolloClient, 'CONNECTION_TIMEOUT')
              }
              observer.error(networkError)
            },
            complete: () => {
              if (!hasApplicationError) {
                // Completed successfully after retrying
                notifications.clearNotification(apolloClient)
              }
              observer.complete.bind(observer)()
            },
          })
        } catch (e) {
          observer.error(e)
        }
        return () => {
          if (subscription) subscription.unsubscribe()
        }
      })
    })