diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 4b487712..01c2097b 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -27,9 +27,11 @@ class QueryManager { required this.link, required this.cache, this.alwaysRebroadcast = false, + bool deduplicatePollers = false, }) { scheduler = QueryScheduler( queryManager: this, + deduplicatePollers: deduplicatePollers, ); } diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 84cd3d59..1db7217b 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -26,11 +26,13 @@ class GraphQLClient implements GraphQLDataProxy { required this.cache, DefaultPolicies? defaultPolicies, bool alwaysRebroadcast = false, + bool deduplicatePollers = false, }) : defaultPolicies = defaultPolicies ?? DefaultPolicies(), queryManager = QueryManager( link: link, cache: cache, alwaysRebroadcast: alwaysRebroadcast, + deduplicatePollers: deduplicatePollers, ); /// The default [Policies] to set for each client action diff --git a/packages/graphql/lib/src/scheduler/scheduler.dart b/packages/graphql/lib/src/scheduler/scheduler.dart index 9d930775..118aa1b9 100644 --- a/packages/graphql/lib/src/scheduler/scheduler.dart +++ b/packages/graphql/lib/src/scheduler/scheduler.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:collection/collection.dart'; +import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/src/core/query_manager.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/observable_query.dart'; @@ -8,9 +10,11 @@ import 'package:graphql/src/core/observable_query.dart'; class QueryScheduler { QueryScheduler({ this.queryManager, - }); + bool deduplicatePollers = false, + }) : _deduplicatePollers = deduplicatePollers; QueryManager? queryManager; + final bool _deduplicatePollers; /// Map going from query ids to the [WatchQueryOptions] associated with those queries. Map registeredQueries = @@ -68,25 +72,81 @@ class QueryScheduler { options.pollInterval != null && options.pollInterval! > Duration.zero, ); - registeredQueries[queryId] = options; + final existingEntry = _fastestEntryForRequest(options.asRequest); + final String? existingQueryId = existingEntry?.key; + final Duration? existingInterval = existingEntry?.value.pollInterval; - final interval = options.pollInterval; + // Update or add the query in registeredQueries + registeredQueries[queryId] = options; - if (intervalQueries.containsKey(interval)) { - intervalQueries[interval]!.add(queryId); + final Duration interval; + + if (existingInterval != null && _deduplicatePollers) { + if (existingInterval > options.pollInterval!) { + // The new one is faster, remove the old one and add the new one + intervalQueries[existingInterval]!.remove(existingQueryId); + interval = options.pollInterval!; + } else { + // The new one is slower or the same. Don't add it to the list + return; + } } else { - intervalQueries[interval] = Set.of([queryId]); - - _pollingTimers[interval] = Timer.periodic( - interval!, - (Timer timer) => fetchQueriesOnInterval(timer, interval), - ); + // If there is no existing interval, we'll add the new one + interval = options.pollInterval!; } + + // Add new query to intervalQueries + _addInterval(queryId, interval); } /// Removes the [ObservableQuery] from one of the registered queries. /// The fetchQueriesOnInterval will then take care of not firing it anymore. void stopPollingQuery(String queryId) { - registeredQueries.remove(queryId); + final removedQuery = registeredQueries.remove(queryId); + + if (removedQuery == null || + removedQuery.pollInterval == null || + !_deduplicatePollers) { + return; + } + + // If there is a registered query that has the same `asRequest` as this one + // Add the next fastest duration to the intervalQueries + final fastestEntry = _fastestEntryForRequest(removedQuery.asRequest); + final String? fastestQueryId = fastestEntry?.key; + final Duration? fastestInterval = fastestEntry?.value.pollInterval; + + if (fastestQueryId == null || fastestInterval == null) { + // There is no other query, return. + return; + } + + _addInterval(fastestQueryId, fastestInterval); + } + + /// Adds a [queryId] to the [intervalQueries] for a specific [interval] + /// and starts the timer if it doesn't exist. + void _addInterval(String queryId, Duration interval) { + final existingSet = intervalQueries[interval]; + if (existingSet != null) { + existingSet.add(queryId); + } else { + intervalQueries[interval] = {queryId}; + _pollingTimers[interval] = Timer.periodic( + interval, (Timer timer) => fetchQueriesOnInterval(timer, interval)); + } + } + + /// Returns the fastest query that matches the [request] or null if none exists. + MapEntry>? _fastestEntryForRequest( + Request request) { + return registeredQueries.entries + // All existing queries mapping to the same request. + .where((entry) => + entry.value.asRequest == request && + entry.value.pollInterval != null) + // Ascending is default (shortest poll interval first) + .sortedBy((entry) => entry.value.pollInterval!) + .firstOrNull; } }