Core Concepts

Deduplication

Qora merges concurrent requests for the same key into a single network call, regardless of the number of callers.

The Problem

Multiple widgets or services can independently request the same data at the same moment: on page load, during a fast navigation, or when a feature flag is evaluated. Without deduplication, each call fires its own network request:

Without deduplication:
  Widget A → GET /users/1  ←─┐
  Widget B → GET /users/1  ←─┤  3 identical network calls
  Widget C → GET /users/1  ←─┘

This wastes bandwidth, adds server load, and risks race conditions where responses arrive out of order.


How Qora Deduplicates

Qora tracks every in-flight request in a _pendingRequests map keyed by the normalised query key. When a second call arrives while the first is still pending:

  1. The in-flight Future is reused. No new network call is issued.
  2. All callers await the same result.
  3. The cache is updated once when the single response arrives.
  4. All active watchQuery streams receive the state update.
With Qora:
  Widget A ─────────────────┐
  Widget B → fetchQuery → dedup → GET /users/1  (1 request)
  Widget C ─────────────────┘
           all three resolve with the same result

Example

main.dart
// All three calls fire simultaneously: only one HTTP request is made
final f1 = client.fetchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));
final f2 = client.fetchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));
final f3 = client.fetchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));

// f1, f2, f3 are the same Future: one network call, three waiters
final results = await Future.wait([f1, f2, f3]);
// results = [alice, alice, alice]

The same applies to watchQuery subscribers:

main.dart
// Mounted simultaneously on the same page
final stream1 = client.watchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));
final stream2 = client.watchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));

// One fetch fires; both streams emit the same Loading → Success sequence

Deduplication Scope

Deduplication applies within a single QoraClient instance. A single shared client per application (or DI scope) provides full deduplication coverage across all widgets and services.

main.dart
// Good: One shared client: full deduplication
final client = QoraClient();

class UserRepository {
  UserRepository(this._client);   // injected
  final QoraClient _client;
}

class PostRepository {
  PostRepository(this._client);   // same instance
  final QoraClient _client;
}
main.dart
// Bad: Two separate clients: no deduplication between them
final clientA = QoraClient();
final clientB = QoraClient(); // different cache, different pending map

Interaction with Retries

Deduplication and retry compose correctly. If the in-flight request fails, all waiters receive the same Failure state after all retry attempts are exhausted:

fetchQuery called by A, B, C (deduplicated to 1 request)
  ↓ attempt 1 → fails
  ↓ wait 1 s
  ↓ attempt 2 → fails
  ↓ wait 2 s
  ↓ attempt 3 → fails
  ↓ Failure stored in cache
  A, B, C all receive Failure

When Deduplication Does Not Apply

Deduplication merges only requests that are in-flight at the same time. Sequential calls are independent and resolved through the cache:

main.dart
// Sequential: two network calls (each hits SWR or cache independently)
final u1 = await client.fetchQuery<User>(key: ['users', 1], fetcher: api.getUser);
final u2 = await client.fetchQuery<User>(key: ['users', 1], fetcher: api.getUser);
//                                                 ↑ served from cache (staleTime check)

Next Steps

Copyright © 2026