Skip to content

Commit

Permalink
feat(graphql): Support custom equality function for cache comparison
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
kvenn committed Aug 5, 2024
1 parent e68adbe commit a6909d4
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 17 deletions.
3 changes: 2 additions & 1 deletion packages/graphql/lib/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export 'package:graphql/src/graphql_client.dart';

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

export 'package:graphql/src/utilities/helpers.dart' show gql;
export 'package:graphql/src/utilities/helpers.dart'
show gql, optimizedDeepEquals;
10 changes: 3 additions & 7 deletions packages/graphql/lib/src/cache/fragment.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'dart:convert';
import "package:graphql/client.dart";
import "package:meta/meta.dart";
import "package:collection/collection.dart";

import "package:gql/ast.dart";
import 'package:gql/language.dart';
import "package:gql_exec/gql_exec.dart";
import 'package:normalize/utils.dart';

/// A fragment in a [document], optionally defined by [fragmentName]
Expand Down Expand Up @@ -32,9 +32,7 @@ class Fragment {
bool operator ==(Object o) =>
identical(this, o) ||
(o is Fragment &&
const ListEquality<Object?>(
DeepCollectionEquality(),
).equals(
gqlDeepEquals(
o._getChildren(),
_getChildren(),
));
Expand Down Expand Up @@ -89,9 +87,7 @@ class FragmentRequest {
bool operator ==(Object o) =>
identical(this, o) ||
(o is FragmentRequest &&
const ListEquality<Object?>(
DeepCollectionEquality(),
).equals(
gqlDeepEquals(
o._getChildren(),
_getChildren(),
));
Expand Down
4 changes: 1 addition & 3 deletions packages/graphql/lib/src/core/_base_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ abstract class BaseOptions<TParsed extends Object?> {
identical(this, other) ||
(other is BaseOptions &&
runtimeType == other.runtimeType &&
const ListEquality<Object?>(
DeepCollectionEquality(),
).equals(
gqlDeepEquals(
other.properties,
properties,
));
Expand Down
4 changes: 1 addition & 3 deletions packages/graphql/lib/src/core/policies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,7 @@ class DefaultPolicies {
bool operator ==(Object o) =>
identical(this, o) ||
(o is DefaultPolicies &&
const ListEquality<Object?>(
DeepCollectionEquality(),
).equals(
gqlDeepEquals(
o._getChildren(),
_getChildren(),
));
Expand Down
17 changes: 14 additions & 3 deletions packages/graphql/lib/src/core/query_manager.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:graphql/src/utilities/helpers.dart';
import 'package:graphql/src/utilities/response.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
Expand All @@ -19,20 +20,29 @@ import 'package:graphql/src/scheduler/scheduler.dart';

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

bool Function(dynamic a, dynamic b) _deepEquals =
const DeepCollectionEquality().equals;
typedef DeepEqualsFn = bool Function(dynamic a, dynamic b);

/// The equality function used for comparing cached and new data.
///
/// You can alternatively provide [optimizedDeepEquals] for a faster
/// equality check. Or provide your own via [GqlClient] constructor.
DeepEqualsFn gqlDeepEquals = const DeepCollectionEquality().equals;

class QueryManager {
QueryManager({
required this.link,
required this.cache,
this.alwaysRebroadcast = false,
DeepEqualsFn? deepEquals,
bool deduplicatePollers = false,
}) {
scheduler = QueryScheduler(
queryManager: this,
deduplicatePollers: deduplicatePollers,
);
if (deepEquals != null) {
gqlDeepEquals = deepEquals;
}
}

final Link link;
Expand Down Expand Up @@ -551,7 +561,8 @@ class QueryManager {
) =>
cachedData != null &&
query.latestResult != null &&
(alwaysRebroadcast || !_deepEquals(query.latestResult!.data, cachedData));
(alwaysRebroadcast ||
!gqlDeepEquals(query.latestResult!.data, cachedData));

void setQuery(ObservableQuery<Object?> observableQuery) {
queries[observableQuery.queryId] = observableQuery;
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/lib/src/graphql_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ class GraphQLClient implements GraphQLDataProxy {
required this.cache,
DefaultPolicies? defaultPolicies,
bool alwaysRebroadcast = false,
DeepEqualsFn? deepEquals,
bool deduplicatePollers = false,
}) : defaultPolicies = defaultPolicies ?? DefaultPolicies(),
queryManager = QueryManager(
link: link,
cache: cache,
alwaysRebroadcast: alwaysRebroadcast,
deepEquals: deepEquals,
deduplicatePollers: deduplicatePollers,
);

Expand Down
43 changes: 43 additions & 0 deletions packages/graphql/lib/src/utilities/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,46 @@ SanitizeVariables variableSanitizer(
toEncodable: sanitizeVariables,
),
) as Map<String, dynamic>;

/// Compare two json-like objects for equality
/// [a] and [b] must be one of
/// - null
/// - num
/// - bool
/// - String
/// - List of above types
/// - Map of above types
///
/// This is an alternative too [DeepCollectionEquality().equals],
/// which is very slow and has O(n^2) complexity:
bool optimizedDeepEquals(Object? a, Object? b) {
if (identical(a, b)) {
return true;
}
if (a == b) {
return true;
}
if (a is Map) {
if (b is! Map) {
return false;
}
if (a.length != b.length) return false;
for (var key in a.keys) {
if (!b.containsKey(key)) return false;
if (!optimizedDeepEquals(a[key], b[key])) return false;
}
return true;
}
if (a is List) {
if (b is! List) {
return false;
}
final length = a.length;
if (length != b.length) return false;
for (var i = 0; i < length; i++) {
if (!optimizedDeepEquals(a[i], b[i])) return false;
}
return true;
}
return false;
}

0 comments on commit a6909d4

Please # to comment.