From 692358af45f8675865fc083bb3ac993a94b667f7 Mon Sep 17 00:00:00 2001 From: Kyle Venn Date: Sat, 3 Aug 2024 20:58:35 -0400 Subject: [PATCH 1/2] Add configurable behavior to deuplicate --- .../graphql/lib/src/core/query_manager.dart | 1 + packages/graphql/lib/src/graphql_client.dart | 1 + .../graphql/lib/src/scheduler/scheduler.dart | 84 ++++++++++++++++--- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 4b4877121..a0601ba44 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -27,6 +27,7 @@ class QueryManager { required this.link, required this.cache, this.alwaysRebroadcast = false, + bool deduplicatePollers = false, }) { scheduler = QueryScheduler( queryManager: this, diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 84cd3d598..ce40e0e6b 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -26,6 +26,7 @@ class GraphQLClient implements GraphQLDataProxy { required this.cache, DefaultPolicies? defaultPolicies, bool alwaysRebroadcast = false, + bool deduplicatePollers = false, }) : defaultPolicies = defaultPolicies ?? DefaultPolicies(), queryManager = QueryManager( link: link, diff --git a/packages/graphql/lib/src/scheduler/scheduler.dart b/packages/graphql/lib/src/scheduler/scheduler.dart index 9d930775f..983c2073c 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; } } From e4967c04e6d01f2e25ff0e2e13c61342d423f25b Mon Sep 17 00:00:00 2001 From: Kyle Venn Date: Sun, 4 Aug 2024 10:52:38 -0400 Subject: [PATCH 2/2] Fix remaining issues --- packages/graphql/lib/src/core/query_manager.dart | 1 + packages/graphql/lib/src/graphql_client.dart | 1 + packages/graphql/lib/src/scheduler/scheduler.dart | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index a0601ba44..01c2097ba 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -31,6 +31,7 @@ class QueryManager { }) { 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 ce40e0e6b..1db7217b4 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -32,6 +32,7 @@ class GraphQLClient implements GraphQLDataProxy { 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 983c2073c..118aa1b94 100644 --- a/packages/graphql/lib/src/scheduler/scheduler.dart +++ b/packages/graphql/lib/src/scheduler/scheduler.dart @@ -105,7 +105,7 @@ class QueryScheduler { final removedQuery = registeredQueries.remove(queryId); if (removedQuery == null || - removedQuery.pollInterval != null || + removedQuery.pollInterval == null || !_deduplicatePollers) { return; }