Skip to content

Commit a6909d4

Browse files
committed
feat(graphql): Support custom equality function for cache comparison
- The current equality check has received some dissatisfaction due to performance (and is a source of jank/missed frames in my application) - This is an unopinionated way to allow those consumers to provide their own equality function with better performance via optimizedDeepEquals
1 parent e68adbe commit a6909d4

File tree

7 files changed

+66
-17
lines changed

7 files changed

+66
-17
lines changed

packages/graphql/lib/client.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export 'package:graphql/src/graphql_client.dart';
99

1010
export 'package:graphql/src/links/links.dart';
1111

12-
export 'package:graphql/src/utilities/helpers.dart' show gql;
12+
export 'package:graphql/src/utilities/helpers.dart'
13+
show gql, optimizedDeepEquals;

packages/graphql/lib/src/cache/fragment.dart

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'dart:convert';
2+
import "package:graphql/client.dart";
23
import "package:meta/meta.dart";
34
import "package:collection/collection.dart";
45

56
import "package:gql/ast.dart";
67
import 'package:gql/language.dart';
7-
import "package:gql_exec/gql_exec.dart";
88
import 'package:normalize/utils.dart';
99

1010
/// A fragment in a [document], optionally defined by [fragmentName]
@@ -32,9 +32,7 @@ class Fragment {
3232
bool operator ==(Object o) =>
3333
identical(this, o) ||
3434
(o is Fragment &&
35-
const ListEquality<Object?>(
36-
DeepCollectionEquality(),
37-
).equals(
35+
gqlDeepEquals(
3836
o._getChildren(),
3937
_getChildren(),
4038
));
@@ -89,9 +87,7 @@ class FragmentRequest {
8987
bool operator ==(Object o) =>
9088
identical(this, o) ||
9189
(o is FragmentRequest &&
92-
const ListEquality<Object?>(
93-
DeepCollectionEquality(),
94-
).equals(
90+
gqlDeepEquals(
9591
o._getChildren(),
9692
_getChildren(),
9793
));

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ abstract class BaseOptions<TParsed extends Object?> {
104104
identical(this, other) ||
105105
(other is BaseOptions &&
106106
runtimeType == other.runtimeType &&
107-
const ListEquality<Object?>(
108-
DeepCollectionEquality(),
109-
).equals(
107+
gqlDeepEquals(
110108
other.properties,
111109
properties,
112110
));

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,7 @@ class DefaultPolicies {
307307
bool operator ==(Object o) =>
308308
identical(this, o) ||
309309
(o is DefaultPolicies &&
310-
const ListEquality<Object?>(
311-
DeepCollectionEquality(),
312-
).equals(
310+
gqlDeepEquals(
313311
o._getChildren(),
314312
_getChildren(),
315313
));

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:graphql/src/utilities/helpers.dart';
34
import 'package:graphql/src/utilities/response.dart';
45
import 'package:meta/meta.dart';
56
import 'package:collection/collection.dart';
@@ -19,20 +20,29 @@ import 'package:graphql/src/scheduler/scheduler.dart';
1920

2021
import 'package:graphql/src/core/_query_write_handling.dart';
2122

22-
bool Function(dynamic a, dynamic b) _deepEquals =
23-
const DeepCollectionEquality().equals;
23+
typedef DeepEqualsFn = bool Function(dynamic a, dynamic b);
24+
25+
/// The equality function used for comparing cached and new data.
26+
///
27+
/// You can alternatively provide [optimizedDeepEquals] for a faster
28+
/// equality check. Or provide your own via [GqlClient] constructor.
29+
DeepEqualsFn gqlDeepEquals = const DeepCollectionEquality().equals;
2430

2531
class QueryManager {
2632
QueryManager({
2733
required this.link,
2834
required this.cache,
2935
this.alwaysRebroadcast = false,
36+
DeepEqualsFn? deepEquals,
3037
bool deduplicatePollers = false,
3138
}) {
3239
scheduler = QueryScheduler(
3340
queryManager: this,
3441
deduplicatePollers: deduplicatePollers,
3542
);
43+
if (deepEquals != null) {
44+
gqlDeepEquals = deepEquals;
45+
}
3646
}
3747

3848
final Link link;
@@ -551,7 +561,8 @@ class QueryManager {
551561
) =>
552562
cachedData != null &&
553563
query.latestResult != null &&
554-
(alwaysRebroadcast || !_deepEquals(query.latestResult!.data, cachedData));
564+
(alwaysRebroadcast ||
565+
!gqlDeepEquals(query.latestResult!.data, cachedData));
555566

556567
void setQuery(ObservableQuery<Object?> observableQuery) {
557568
queries[observableQuery.queryId] = observableQuery;

packages/graphql/lib/src/graphql_client.dart

+2
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ class GraphQLClient implements GraphQLDataProxy {
2626
required this.cache,
2727
DefaultPolicies? defaultPolicies,
2828
bool alwaysRebroadcast = false,
29+
DeepEqualsFn? deepEquals,
2930
bool deduplicatePollers = false,
3031
}) : defaultPolicies = defaultPolicies ?? DefaultPolicies(),
3132
queryManager = QueryManager(
3233
link: link,
3334
cache: cache,
3435
alwaysRebroadcast: alwaysRebroadcast,
36+
deepEquals: deepEquals,
3537
deduplicatePollers: deduplicatePollers,
3638
);
3739

packages/graphql/lib/src/utilities/helpers.dart

+43
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,46 @@ SanitizeVariables variableSanitizer(
9090
toEncodable: sanitizeVariables,
9191
),
9292
) as Map<String, dynamic>;
93+
94+
/// Compare two json-like objects for equality
95+
/// [a] and [b] must be one of
96+
/// - null
97+
/// - num
98+
/// - bool
99+
/// - String
100+
/// - List of above types
101+
/// - Map of above types
102+
///
103+
/// This is an alternative too [DeepCollectionEquality().equals],
104+
/// which is very slow and has O(n^2) complexity:
105+
bool optimizedDeepEquals(Object? a, Object? b) {
106+
if (identical(a, b)) {
107+
return true;
108+
}
109+
if (a == b) {
110+
return true;
111+
}
112+
if (a is Map) {
113+
if (b is! Map) {
114+
return false;
115+
}
116+
if (a.length != b.length) return false;
117+
for (var key in a.keys) {
118+
if (!b.containsKey(key)) return false;
119+
if (!optimizedDeepEquals(a[key], b[key])) return false;
120+
}
121+
return true;
122+
}
123+
if (a is List) {
124+
if (b is! List) {
125+
return false;
126+
}
127+
final length = a.length;
128+
if (length != b.length) return false;
129+
for (var i = 0; i < length; i++) {
130+
if (!optimizedDeepEquals(a[i], b[i])) return false;
131+
}
132+
return true;
133+
}
134+
return false;
135+
}

0 commit comments

Comments
 (0)