Skip to content

Commit 3291a76

Browse files
committed
Reuse cached query results
1 parent 0b1b327 commit 3291a76

File tree

4 files changed

+107
-28
lines changed

4 files changed

+107
-28
lines changed

packages/graphql/lib/src/core/observable_query.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:async';
2+
import 'package:gql/ast.dart';
23
import 'package:graphql/client.dart';
4+
import 'package:graphql/src/core/_base_options.dart';
35
import 'package:meta/meta.dart';
46

57
import 'package:graphql/src/core/fetch_more.dart';
@@ -242,12 +244,13 @@ class ObservableQuery<TParsed> {
242244
}
243245

244246
/// Add a [result] to the [stream] unless it was created
245-
/// before [lasestResult].
247+
/// before [latestResult].
246248
///
247249
/// Copies the [QueryResult.source] from the [latestResult]
248250
/// if it is set to `null`.
249251
///
250-
/// Called internally by the [QueryManager]
252+
/// Called internally by the [QueryManager]. Do not call this directly except
253+
/// for [QueryResult.loading]
251254
void addResult(QueryResult<TParsed> result, {bool fromRebroadcast = false}) {
252255
// don't overwrite results due to some async/optimism issue
253256
if (latestResult != null &&

packages/graphql/lib/src/core/query_manager.dart

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:gql/ast.dart';
34
import 'package:graphql/src/utilities/response.dart';
45
import 'package:meta/meta.dart';
56
import 'package:collection/collection.dart';
@@ -311,14 +312,26 @@ class QueryManager {
311312
// we attempt to resolve the from the cache
312313
if (shouldRespondEagerlyFromCache(options.fetchPolicy) &&
313314
!queryResult.isOptimistic) {
314-
final data = cache.readQuery(request, optimistic: false);
315-
// we only push an eager query with data
316-
if (data != null) {
317-
queryResult = QueryResult(
318-
options: options,
319-
data: data,
315+
final latestResult = _getQueryResultByRequest<TParsed>(request);
316+
if (latestResult != null && latestResult.data != null) {
317+
// we have a result already cached + deserialized for this request
318+
// so we reuse it.
319+
// latest result won't be for loading, it must contain data
320+
queryResult = latestResult.copyWith(
320321
source: QueryResultSource.cache,
321322
);
323+
} else {
324+
// otherwise, we try to find the query in cache (which will require
325+
// deserialization)
326+
final data = cache.readQuery(request, optimistic: false);
327+
// we only push an eager query with data
328+
if (data != null) {
329+
queryResult = QueryResult(
330+
options: options,
331+
data: data,
332+
source: QueryResultSource.cache,
333+
);
334+
}
322335
}
323336

324337
if (options.fetchPolicy == FetchPolicy.cacheOnly &&
@@ -358,6 +371,18 @@ class QueryManager {
358371
return queryResult;
359372
}
360373

374+
/// If a request already has a result associated with it in cache (as
375+
/// determined by [ObservableQuery.latestResult]), we can return it without
376+
/// needing to denormalize + parse again.
377+
QueryResult<TParsed>? _getQueryResultByRequest<TParsed>(Request request) {
378+
for (final query in queries.values) {
379+
if (query.options.asRequest == request) {
380+
return query.latestResult as QueryResult<TParsed>?;
381+
}
382+
}
383+
return null;
384+
}
385+
361386
/// Refetch the [ObservableQuery] referenced by [queryId],
362387
/// overriding any present non-network-only [FetchPolicy].
363388
Future<QueryResult<TParsed>?> refetchQuery<TParsed>(String queryId) {
@@ -383,11 +408,11 @@ class QueryManager {
383408
return results;
384409
}
385410

386-
ObservableQuery<TParsed>? getQuery<TParsed>(String? queryId) {
387-
if (!queries.containsKey(queryId)) {
411+
ObservableQuery<TParsed>? getQuery<TParsed>(final String? queryId) {
412+
if (!queries.containsKey(queryId) || queryId == null) {
388413
return null;
389414
}
390-
final query = queries[queryId!];
415+
final query = queries[queryId];
391416
if (query is ObservableQuery<TParsed>) {
392417
return query;
393418
}
@@ -402,16 +427,17 @@ class QueryManager {
402427
void addQueryResult<TParsed>(
403428
Request request,
404429
String? queryId,
405-
QueryResult<TParsed> queryResult,
406-
) {
430+
QueryResult<TParsed> queryResult, {
431+
bool fromRebroadcast = false,
432+
}) {
407433
final observableQuery = getQuery<TParsed>(queryId);
408434

409435
if (observableQuery != null && !observableQuery.controller.isClosed) {
410-
observableQuery.addResult(queryResult);
436+
observableQuery.addResult(queryResult, fromRebroadcast: fromRebroadcast);
411437
}
412438
}
413439

414-
/// Create an optimstic result for the query specified by `queryId`, if it exists
440+
/// Create an optimistic result for the query specified by `queryId`, if it exists
415441
QueryResult<TParsed> _getOptimisticQueryResult<TParsed>(
416442
Request request, {
417443
required String queryId,
@@ -463,27 +489,55 @@ class QueryManager {
463489
return false;
464490
}
465491

466-
final shouldBroadast = cache.shouldBroadcast(claimExecution: true);
492+
final shouldBroadcast = cache.shouldBroadcast(claimExecution: true);
467493

468-
if (!shouldBroadast && !force) {
494+
if (!shouldBroadcast && !force) {
469495
return false;
470496
}
471497

472-
for (var query in queries.values) {
473-
if (query != exclude && query.isRebroadcastSafe) {
498+
// If two ObservableQueries are backed by the same [Request], we only need
499+
// to [readQuery] for it once.
500+
final Map<Request, QueryResult<Object?>> diffQueryResultCache = {};
501+
final Map<Request, bool> ignoreQueryResults = {};
502+
for (final query in queries.values) {
503+
final Request request = query.options.asRequest;
504+
final cachedQueryResult = diffQueryResultCache[request];
505+
if (query == exclude || !query.isRebroadcastSafe) {
506+
continue;
507+
}
508+
if (cachedQueryResult != null) {
509+
// We've already done the diff and denormalized, emit to the observable
510+
addQueryResult(
511+
request,
512+
query.queryId,
513+
cachedQueryResult,
514+
fromRebroadcast: true,
515+
);
516+
} else if (ignoreQueryResults.containsKey(request)) {
517+
// We've already seen this one and don't need to notify
518+
continue;
519+
} else {
520+
// We haven't seen this one yet, denormalize from cache and diff
474521
final cachedData = cache.readQuery(
475522
query.options.asRequest,
476523
optimistic: query.options.policies.mergeOptimisticData,
477524
);
478525
if (_cachedDataHasChangedFor(query, cachedData)) {
479-
query.addResult(
480-
mapFetchResultToQueryResult(
481-
Response(data: cachedData, response: {}),
482-
query.options,
483-
source: QueryResultSource.cache,
484-
),
526+
// The data has changed
527+
final queryResult = QueryResult(
528+
data: cachedData,
529+
options: query.options,
530+
source: QueryResultSource.cache,
531+
);
532+
diffQueryResultCache[request] = queryResult;
533+
addQueryResult(
534+
request,
535+
query.queryId,
536+
queryResult,
485537
fromRebroadcast: true,
486538
);
539+
} else {
540+
ignoreQueryResults[request] = true;
487541
}
488542
}
489543
}

packages/graphql/lib/src/core/query_options.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,13 @@ class WatchQueryOptions<TParsed extends Object?> extends QueryOptions<TParsed> {
168168
parserFn: parserFn,
169169
);
170170

171-
/// Whether or not to fetch results
171+
/// Whether or not to fetch results every time a new listener is added.
172+
/// If [eagerlyFetchResults] is `true`, fetch is triggered during instantiation.
172173
final bool fetchResults;
173174

174-
/// Whether to [fetchResults] immediately on instantiation.
175+
/// Whether to [fetchResults] immediately on instantiation of [ObservableQuery].
176+
/// The first
177+
/// If available, cache results are emitted when the first listener is added.
175178
/// Defaults to [fetchResults].
176179
final bool eagerlyFetchResults;
177180

packages/graphql/lib/src/core/query_result.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class QueryResult<TParsed extends Object?> {
4848
this.context = const Context(),
4949
required this.parserFn,
5050
required this.source,
51-
}) : timestamp = DateTime.now() {
51+
TParsed? cachedParsedData,
52+
}) : timestamp = DateTime.now(),
53+
_cachedParsedData = cachedParsedData {
5254
_data = data;
5355
}
5456

@@ -160,6 +162,23 @@ class QueryResult<TParsed extends Object?> {
160162
return _cachedParsedData = parserFn(data);
161163
}
162164

165+
QueryResult<TParsed> copyWith({
166+
Map<String, dynamic>? data,
167+
OperationException? exception,
168+
Context? context,
169+
QueryResultSource? source,
170+
TParsed? cachedParsedData,
171+
}) {
172+
return QueryResult.internal(
173+
data: data ?? this.data,
174+
exception: exception ?? this.exception,
175+
context: context ?? this.context,
176+
parserFn: parserFn,
177+
source: source ?? this.source,
178+
cachedParsedData: cachedParsedData ?? _cachedParsedData,
179+
);
180+
}
181+
163182
@override
164183
String toString() => 'QueryResult('
165184
'source: $source, '

0 commit comments

Comments
 (0)