fetch method
- DefaultedQueryOptions<
TQueryData> ? options, - 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;
}