Deduplication
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:
- The in-flight
Futureis reused. No new network call is issued. - All callers await the same result.
- The cache is updated once when the single response arrives.
- All active
watchQuerystreams 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
// 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:
// 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.
// 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;
}
// 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:
// 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
- Caching: How the cache stores and ages entries
- Stale-While-Revalidate: Background revalidation
- Mutations & Invalidation: Writes and how they update the cache
- QoraClient API: Full client reference