API Reference

QoraClient

The central engine of Qora: manages queries, cache, deduplication, retries, and reactive state

Constructor

main.dart
QoraClient({QoraClientConfig? config})

Creates a new client instance. All queries share the same cache and deduplication map.

main.dart
// Minimal (all defaults)
final client = QoraClient();

// With full configuration
final client = QoraClient(
  config: QoraClientConfig(
    defaultOptions: QoraOptions(
      staleTime: Duration(minutes: 5),
      retryCount: 2,
    ),
    debugMode: kDebugMode,
    maxCacheSize: 200,
  ),
);

Properties

config

final QoraClientConfig config

The resolved configuration for this client instance. Read-only.

cachedKeys

Iterable<List<dynamic>> get cachedKeys

All currently cached query keys (normalised). Useful for debugging or bulk invalidation.

main.dart
for (final key in client.cachedKeys) {
  print(key); // [users, 1], [posts], ...
}

Fetching

fetchQuery

Future<T> fetchQuery<T>({
  required Object key,
  required Future<T> Function() fetcher,
  QoraOptions? options,
  CancelToken? cancelToken,
})

Fetch data once and return the result.

Cache behaviour:

Cache stateAction
Fresh (not stale)Return cached data: no network call
StaleReturn stale data immediately; revalidate in background
MissingFetch, await, and return result

Concurrent calls with the same key are deduplicated: all share a single in-flight request. Failed fetches are retried with exponential backoff up to retryCount times.

user_repository.dart
final user = await client.fetchQuery<User>(
  key: ['users', userId],
  fetcher: () => api.getUser(userId),
  options: QoraOptions(staleTime: Duration(minutes: 5)),
);

Throws:

  • StateError if QoraOptions.enabled is false
  • StateError if QoraOptions.dependsOn is set and the dependency has no data yet
  • QoraCancelException if cancelToken is cancelled before or during the fetch
  • The fetcher's exception (after all retries) on failure

watchQuery

Stream<QoraState<T>> watchQuery<T>({
  required Object key,
  required Future<T> Function() fetcher,
  QoraOptions? options,
  CancelToken? cancelToken,
})

Create a reactive Stream<QoraState<T>> for a query.

On subscription:

  1. Emits the current cached state immediately
  2. Triggers a fetch if data is missing or stale (per refetchOnMount)
  3. Sets up a polling timer if refetchInterval is set
  4. Pushes all future state changes: from any source

On cancel (last subscriber):

  • Cancels the polling timer
  • Schedules a GC timer for the cache entry
user_page.dart
StreamBuilder<QoraState<User>>(
  stream: client.watchQuery<User>(
    key: ['users', userId],
    fetcher: () => api.getUser(userId),
    options: QoraOptions(refetchInterval: Duration(seconds: 30)),
  ),
  builder: (context, snapshot) {
    return switch (snapshot.data) {
      null || Initial()             => const SizedBox.shrink(),
      Loading(:final previousData)  => previousData != null
          ? UserCard(previousData)
          : const CircularProgressIndicator(),
      Success(:final data)          => UserCard(data),
      Failure(:final error)         => ErrorWidget('$error'),
    };
  },
)

prefetch

Future<void> prefetch<T>({
  required Object key,
  required Future<T> Function() fetcher,
  QoraOptions? options,
  CancelToken? cancelToken,
})

Pre-warm the cache without creating a stream. No-op if data is already fresh.

user_list_page.dart
onEnter: (_) => client.prefetch<User>(
  key: ['users', userId],
  fetcher: () => api.getUser(userId),
),

Cache Reads

getQueryData

T? getQueryData<T>(Object key)

Returns the best available data for key, or null. Returns Success.data, or previousData from Loading/Failure.

main.dart
final user = client.getQueryData<User>(['users', userId]);

getQueryState

QoraState<T> getQueryState<T>(Object key)

Returns the full QoraState<T>, or Initial<T>() if not cached.

main.dart
final state = client.getQueryState<User>(['users', userId]);
if (state is Success<User>) {
  print('Age: ${state.age}');
}

Cache Writes

setQueryData

void setQueryData<T>(Object key, T data)

Inject data directly into the cache. Instantly pushes Success to all active watchQuery streams.

main.dart
client.setQueryData(['users', userId], updatedUser);

restoreQueryData

void restoreQueryData<T>(Object key, T? snapshot)

Roll back to a previous snapshot. If snapshot is null, the entry is removed from cache.

main.dart
final snapshot = client.getQueryData<User>(['users', userId]);
client.setQueryData(['users', userId], optimisticUser);

try {
  await api.updateUser(userId, payload);
} catch (_) {
  client.restoreQueryData(['users', userId], snapshot);
}

Invalidation

invalidate

void invalidate(Object key)

Mark a query as stale. Active watchQuery streams receive Loading(previousData: ...) immediately. The next access triggers a fresh fetch.

main.dart
await api.createPost(payload);
client.invalidate(['posts']);

invalidateWhere

void invalidateWhere(bool Function(List<dynamic> key) predicate)

Invalidate all queries whose key satisfies predicate.

main.dart
// All user queries
client.invalidateWhere((key) => key.firstOrNull == 'users');

// Specific user + their sub-resources
client.invalidateWhere(
  (key) => key.length >= 2 && key[0] == 'users' && key[1] == userId,
);

invalidatePrefix

void invalidatePrefix(Object prefix)

Invalidate every query whose key starts with prefix. Segments are compared with the same deep equality as query keys (maps and nested lists match structurally). An empty prefix matches every query. Active watchers refetch.

main.dart
// ['posts'], ['posts', 1], ['posts', 1, 'comments'], ...
client.invalidatePrefix(['posts']);

// Every query under user 42
client.invalidatePrefix(['users', 42]);
invalidate(key) is exact. Use invalidatePrefix when you want TanStack-style partial matching.

invalidateQueries

void invalidateQueries({required QueryFilter filter})

Bulk invalidation with a QueryFilter: a richer predicate than invalidateWhere that also receives the current state and the options from the last successful fetch.

typedef QueryFilter = bool Function(
  String key,
  QoraState<dynamic> state,
  QoraOptions? lastOptions,
);
main.dart
// Invalidate every online-mode query not yet in Success
client.invalidateQueries(
  filter: (key, state, opts) =>
      opts?.networkMode == NetworkMode.online && state is! Success,
);

// Invalidate all user queries whose data is older than 10 minutes
client.invalidateQueries(
  filter: (key, state, opts) {
    if (state is! Success) return false;
    return key.contains('users') &&
        DateTime.now().difference(state.updatedAt) > const Duration(minutes: 10);
  },
);

invalidateTags

void invalidateTags(List<QueryTag> tags)

Invalidate all cached queries that provide at least one of the given tags. Queries register tags via QoraOptions.providesTags. Matching follows two rules:

  • Wildcard: QueryTag('post') matches every query that provides any tag with type: 'post', regardless of id.
  • Specific: QueryTag('post', '123') matches only queries that provide a tag with type: 'post' and id: '123'.
main.dart
// Refetch all post queries
client.invalidateTags([QueryTag('post')]);

// Refetch a specific post + its author
client.invalidateTags([QueryTag('post', postId), QueryTag('user', authorId)]);

See QoraOptions.providesTags and MutationOptions.invalidatesTags.


Cache Removal

removeQuery

void removeQuery(Object key)

Remove an entry from cache and cancel any pending request. The next access starts from Initial.

main.dart
await api.deletePost(postId);
client.removeQuery(['posts', postId]);

clearCache

void clearCache()

Clear the in-memory cache only. Persisted storage is untouched. Use clear() to wipe both (if using persistence), or clearStorage() to wipe storage only.


clear

void clear()

Remove all cached queries and cancel all pending requests. For QoraClient, this wipes the in-memory cache (identical to clearCache). For PersistQoraClient, this wipes BOTH in-memory cache and storage.

main.dart
// On logout
client.clear();

Hydration

queueHydration

void queueHydration(Object key, dynamic data, {DateTime? updatedAt})

Enqueue a pre-deserialized value for lazy injection into the typed cache. The value is injected on the first typed API call (fetchQuery<T>, watchQuery<T>, etc.) for that key, deferring the Dart cast until the type T is known.

This is the shared mechanism used by PersistQoraClient.hydrate() and SsrHydrator.hydrate(). Direct use is for advanced scenarios (e.g. custom hydration sources).

main.dart
// Manually pre-populate the cache before runApp
client.queueHydration(['users', 1], User(id: 1, name: 'Ada'));

// With a custom timestamp so staleTime evaluates correctly
client.queueHydration(
  ['config'],
  appConfig,
  updatedAt: DateTime.now().subtract(const Duration(minutes: 2)),
);

hydrateQuery

void hydrateQuery<T>(Object key, T data, {DateTime? updatedAt})

Inject a typed Success<T> directly into an Initial cache entry. No-op if the entry already has data. Used internally; prefer queueHydration when the type is not known at call time.


Debugging

debugInfo

Map<String, dynamic> debugInfo()

Returns a debug snapshot of the current cache state.

main.dart
print(client.debugInfo());
// {
//   total_queries: 12,
//   active_queries: 3,
//   inactive_queries: 9,
//   pending_requests: 1,
//   max_size: 200,
//   keys: [['users', 1], ['posts'], ...]
// }

Lifecycle

attachLifecycleManager

void attachLifecycleManager(LifecycleManager manager)

Registers a LifecycleManager with the client. When the app transitions to LifecycleState.resumed, the client revalidates all stale entries that have active subscribers. QoraScope calls this automatically when a FlutterLifecycleManager is provided. Direct use is only necessary in custom embedding scenarios.


dispose

void dispose()

Release all resources. Stops the eviction timer, disposes all cache entries, cancels pending requests. The client must not be used after this call.

main.dart
// In a DI teardown or test tearDown
client.dispose();

lib/core/qora.dart
import 'package:flutter/foundation.dart';
import 'package:qora/qora.dart';

/// Singleton: create once, inject everywhere via DI or QoraScope.
final qoraClient = QoraClient(
  config: QoraClientConfig(
    defaultOptions: QoraOptions(
      staleTime: Duration(minutes: 5),
      gcTime: Duration(minutes: 10),
      retryCount: 3,
    ),
    debugMode: kDebugMode,
    maxCacheSize: 200,
    refetchOnMount: true,
    errorMapper: (error, stack) => QoraException(
      '$error',
      originalError: error,
      stackTrace: stack,
    ),
  ),
);
Copyright © 2026