From a6909d47d70f3c6fb25117419620f300d1006432 Mon Sep 17 00:00:00 2001 From: Kyle Venn Date: Wed, 15 May 2024 10:25:59 -0400 Subject: [PATCH] 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 --- packages/graphql/lib/client.dart | 3 +- packages/graphql/lib/src/cache/fragment.dart | 10 ++--- .../graphql/lib/src/core/_base_options.dart | 4 +- packages/graphql/lib/src/core/policies.dart | 4 +- .../graphql/lib/src/core/query_manager.dart | 17 ++++++-- packages/graphql/lib/src/graphql_client.dart | 2 + .../graphql/lib/src/utilities/helpers.dart | 43 +++++++++++++++++++ 7 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 1428bd2fb..e349600bd 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -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; diff --git a/packages/graphql/lib/src/cache/fragment.dart b/packages/graphql/lib/src/cache/fragment.dart index 302ca3591..26608e998 100644 --- a/packages/graphql/lib/src/cache/fragment.dart +++ b/packages/graphql/lib/src/cache/fragment.dart @@ -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] @@ -32,9 +32,7 @@ class Fragment { bool operator ==(Object o) => identical(this, o) || (o is Fragment && - const ListEquality( - DeepCollectionEquality(), - ).equals( + gqlDeepEquals( o._getChildren(), _getChildren(), )); @@ -89,9 +87,7 @@ class FragmentRequest { bool operator ==(Object o) => identical(this, o) || (o is FragmentRequest && - const ListEquality( - DeepCollectionEquality(), - ).equals( + gqlDeepEquals( o._getChildren(), _getChildren(), )); diff --git a/packages/graphql/lib/src/core/_base_options.dart b/packages/graphql/lib/src/core/_base_options.dart index ecd816c47..fa3fcb624 100644 --- a/packages/graphql/lib/src/core/_base_options.dart +++ b/packages/graphql/lib/src/core/_base_options.dart @@ -104,9 +104,7 @@ abstract class BaseOptions { identical(this, other) || (other is BaseOptions && runtimeType == other.runtimeType && - const ListEquality( - DeepCollectionEquality(), - ).equals( + gqlDeepEquals( other.properties, properties, )); diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart index dfe9ddb84..f14a6d1f5 100644 --- a/packages/graphql/lib/src/core/policies.dart +++ b/packages/graphql/lib/src/core/policies.dart @@ -307,9 +307,7 @@ class DefaultPolicies { bool operator ==(Object o) => identical(this, o) || (o is DefaultPolicies && - const ListEquality( - DeepCollectionEquality(), - ).equals( + gqlDeepEquals( o._getChildren(), _getChildren(), )); diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 69802111a..1a7351fb2 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -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'; @@ -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; @@ -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 observableQuery) { queries[observableQuery.queryId] = observableQuery; diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 1db7217b4..357419b81 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -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, ); diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 1784e5a4f..0dce7177a 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -90,3 +90,46 @@ SanitizeVariables variableSanitizer( toEncodable: sanitizeVariables, ), ) as Map; + +/// 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; +}