fetch method

Future<TQueryData> fetch({
  1. DefaultedQueryOptions<TQueryData>? options,
  2. FetchOptions? fetchOptions,
})

Fetches this query now: runs its query function, with the retries and network rules of its options, and completes with the data.

A fetch already in flight is joined rather than duplicated, unless fetchOptions asks for cancelRefetch on a query that holds data. options, when given, replace the query's options first. Every caller gets the same future — it settles once the data has been written to the cache and the cache hooks have run, or once the error is in the state — whether it started the fetch or joined one already running. A query with no query function anywhere fails with MissingQueryFunctionError; a removed query fails with a CancelledError.

Application code usually fetches through QueryClient.query, an observer's refetch, or QueryClient.refetchQueries, which call this.

The fetch is registered before it is announced to observers and cache listeners, so a listener that reacts to it (a cancelQueries, a client.query of the same key, a clear()) finds it to cancel or to join.

Implementation

Future<TQueryData> fetch({
  DefaultedQueryOptions<TQueryData>? options,
  FetchOptions? fetchOptions,
}) {
  if (_removed) {
    return Future<TQueryData>.error(const CancelledError(silent: true));
  }
  if (_state.fetchStatus != FetchStatus.idle &&
      _retryer?.status != RetryerStatus.rejected) {
    if (_state.hasData && (fetchOptions?.cancelRefetch ?? false)) {
      // Deliberately not awaited, as upstream does not: the replacement
      // retryer has to be installed before the cancelled fetch's `catch`
      // runs, or that fetch has nothing to piggyback on and rejects.
      cancel(silent: true).ignore();
    } else {
      final retryer = _retryer;
      final operation = _operation;
      if (retryer != null && operation != null) {
        // Retries stopped by an unmount can continue.
        retryer.continueRetry();
        return operation.future;
      }
    }
  }

  final generation = ++_fetchGeneration;
  Future<TQueryData>? superseded() {
    if (_removed || generation != _fetchGeneration) {
      return _removed
          ? Future<TQueryData>.error(const CancelledError(silent: true))
          : _operation?.future ??
              Future<TQueryData>.error(const CancelledError(silent: true));
    }
    return null;
  }

  if (options != null) {
    setOptions(options);
  }
  final afterOptions = superseded();
  if (afterOptions != null) return afterOptions;

  // A query created by setQueryData or restored from persistence has no
  // query function of its own; borrow one from an observer. A behaviour
  // counts as one: an infinite query's options carry no `queryFn` — its
  // pages come from the behaviour — where upstream's carry the page
  // function as `queryFn`. Checking `queryFn` alone, a plain fetch of an
  // infinite key (`client.query` without a function, a select-only reader)
  // stripped the paging and failed with `MissingQueryFunctionError`, and so
  // did every option-less refetch after it.
  if (_options.queryFn == null && _options.behavior == null) {
    for (final observer in _observers) {
      final observerOptions = observer.observerQueryOptions;
      if (observerOptions.queryFn != null ||
          observerOptions.behavior != null) {
        setOptions(observerOptions as DefaultedQueryOptions<TQueryData>);
        break;
      }
    }
  }

  final afterBorrow = superseded();
  if (afterBorrow != null) return afterBorrow;

  final cancelToken = QueryCancelToken();
  _activeSignal = cancelToken;
  _signalConsumed = false;
  void signalRead() {
    if (identical(_activeSignal, cancelToken)) _signalConsumed = true;
  }

  Future<TQueryData> runQueryFn() async {
    final queryFn = _options.queryFn;
    if (queryFn == null) {
      throw MissingQueryFunctionError(queryKey);
    }
    final context = QueryFunctionContext(
      client: client,
      queryKey: queryKey,
      signal: cancelToken,
      meta: _options.meta,
      onSignalRead: signalRead,
    );
    // Reset per attempt, exactly where upstream resets it: a retry that
    // never touches the token is as uncancellable as a first try that
    // did not.
    if (identical(_activeSignal, cancelToken)) _signalConsumed = false;
    return queryFn(context);
  }

  final context = FetchContext<TQueryData>(
    client: client,
    queryKey: queryKey,
    options: _options,
    state: _state,
    fetchOptions: fetchOptions,
    fetchFn: runQueryFn,
    signal: cancelToken,
    onSignalRead: signalRead,
  );

  _options.behavior?.onFetch(context, this);
  final afterBehavior = superseded();
  if (afterBehavior != null) return afterBehavior;

  // Whether the attempt can only end in `MissingQueryFunctionError`: no
  // query function, and no behaviour that replaced the fetch with its own
  // (an infinite query has no `queryFn` either; its pages come from
  // `pageFn`). Decided here, where a query is known from a mutation, rather
  // than in the retryer.
  final missingQueryFn =
      _options.queryFn == null && identical(context.fetchFn, runQueryFn);

  // Kept in case this fetch has to be reverted.
  _revertState = _state;

  // The CancelledError this fetch's own `cancel` produced, recognised by
  // instance in `_settle`.
  CancelledError? ownCancel;

  // Constructing a retryer runs nothing; `start()` does, below.
  final retryer = Retryer<TQueryData>(
    fn: context.fetchFn,
    focusManager: client.focusManager,
    onlineManager: client.onlineManager,
    canRun: () => true,
    // A missing query function is a configuration error, and retrying it
    // only delays the message by the whole backoff. Upstream retries it
    // like any other failure; here the one attempt is the answer. A
    // per-fetch `retry` outranks the options'.
    retry: missingQueryFn
        ? RetryPolicy.never
        : fetchOptions?.retry ?? _options.retry,
    retryDelay: _options.retryDelay,
    networkMode: _options.networkMode,
    onFail: (failureCount, error, stackTrace) =>
        _dispatch(QueryFailedAction(failureCount, error, stackTrace)),
    onPause: () => _dispatch(const QueryPauseAction()),
    onContinue: () => _dispatch(const QueryContinueAction()),
    onCancel: (error) {
      ownCancel = error;
      if (error.revert) {
        final revertState = _revertState;
        if (revertState != null) {
          setState(revertState.copyWith(fetchStatus: FetchStatus.idle));
        }
      }
      cancelToken.cancel();
    },
  );
  final operation = Completer<TQueryData>();
  _retryer = retryer;
  _operation = operation;

  // Unconditional. Upstream skips the action when `fetchStatus !== 'idle'
  // && fetchMeta === meta`, and that never holds: a `null` fetchMeta is not
  // an unset `undefined`, and a page fetch builds a fresh meta object per
  // call. Comparing value-equal `FetchMore`s (or two `null`s) here skipped
  // it, so a refetch that cancelled a retrying fetch kept the old
  // `fetchFailureCount`.
  _dispatch(QueryFetchAction(meta: fetchOptions?.meta));

  _settle(retryer, operation, () => ownCancel).ignore();
  return operation.future;
}