Skip to content

Commit e68adbe

Browse files
Merge pull request zino-hofmann#1451 from sodality-tech/dedup-poller-zino
feat(graphql): Configurable option to deduplicate pollers
2 parents b365a83 + e4967c0 commit e68adbe

File tree

3 files changed

+76
-12
lines changed

3 files changed

+76
-12
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ class QueryManager {
2727
required this.link,
2828
required this.cache,
2929
this.alwaysRebroadcast = false,
30+
bool deduplicatePollers = false,
3031
}) {
3132
scheduler = QueryScheduler(
3233
queryManager: this,
34+
deduplicatePollers: deduplicatePollers,
3335
);
3436
}
3537

packages/graphql/lib/src/graphql_client.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ class GraphQLClient implements GraphQLDataProxy {
2626
required this.cache,
2727
DefaultPolicies? defaultPolicies,
2828
bool alwaysRebroadcast = false,
29+
bool deduplicatePollers = false,
2930
}) : defaultPolicies = defaultPolicies ?? DefaultPolicies(),
3031
queryManager = QueryManager(
3132
link: link,
3233
cache: cache,
3334
alwaysRebroadcast: alwaysRebroadcast,
35+
deduplicatePollers: deduplicatePollers,
3436
);
3537

3638
/// The default [Policies] to set for each client action

packages/graphql/lib/src/scheduler/scheduler.dart

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:async';
22

3+
import 'package:collection/collection.dart';
4+
import 'package:gql_exec/gql_exec.dart';
35
import 'package:graphql/src/core/query_manager.dart';
46
import 'package:graphql/src/core/query_options.dart';
57
import 'package:graphql/src/core/observable_query.dart';
@@ -8,9 +10,11 @@ import 'package:graphql/src/core/observable_query.dart';
810
class QueryScheduler {
911
QueryScheduler({
1012
this.queryManager,
11-
});
13+
bool deduplicatePollers = false,
14+
}) : _deduplicatePollers = deduplicatePollers;
1215

1316
QueryManager? queryManager;
17+
final bool _deduplicatePollers;
1418

1519
/// Map going from query ids to the [WatchQueryOptions] associated with those queries.
1620
Map<String, WatchQueryOptions> registeredQueries =
@@ -68,25 +72,81 @@ class QueryScheduler {
6872
options.pollInterval != null && options.pollInterval! > Duration.zero,
6973
);
7074

71-
registeredQueries[queryId] = options;
75+
final existingEntry = _fastestEntryForRequest(options.asRequest);
76+
final String? existingQueryId = existingEntry?.key;
77+
final Duration? existingInterval = existingEntry?.value.pollInterval;
7278

73-
final interval = options.pollInterval;
79+
// Update or add the query in registeredQueries
80+
registeredQueries[queryId] = options;
7481

75-
if (intervalQueries.containsKey(interval)) {
76-
intervalQueries[interval]!.add(queryId);
82+
final Duration interval;
83+
84+
if (existingInterval != null && _deduplicatePollers) {
85+
if (existingInterval > options.pollInterval!) {
86+
// The new one is faster, remove the old one and add the new one
87+
intervalQueries[existingInterval]!.remove(existingQueryId);
88+
interval = options.pollInterval!;
89+
} else {
90+
// The new one is slower or the same. Don't add it to the list
91+
return;
92+
}
7793
} else {
78-
intervalQueries[interval] = Set<String>.of([queryId]);
79-
80-
_pollingTimers[interval] = Timer.periodic(
81-
interval!,
82-
(Timer timer) => fetchQueriesOnInterval(timer, interval),
83-
);
94+
// If there is no existing interval, we'll add the new one
95+
interval = options.pollInterval!;
8496
}
97+
98+
// Add new query to intervalQueries
99+
_addInterval(queryId, interval);
85100
}
86101

87102
/// Removes the [ObservableQuery] from one of the registered queries.
88103
/// The fetchQueriesOnInterval will then take care of not firing it anymore.
89104
void stopPollingQuery(String queryId) {
90-
registeredQueries.remove(queryId);
105+
final removedQuery = registeredQueries.remove(queryId);
106+
107+
if (removedQuery == null ||
108+
removedQuery.pollInterval == null ||
109+
!_deduplicatePollers) {
110+
return;
111+
}
112+
113+
// If there is a registered query that has the same `asRequest` as this one
114+
// Add the next fastest duration to the intervalQueries
115+
final fastestEntry = _fastestEntryForRequest(removedQuery.asRequest);
116+
final String? fastestQueryId = fastestEntry?.key;
117+
final Duration? fastestInterval = fastestEntry?.value.pollInterval;
118+
119+
if (fastestQueryId == null || fastestInterval == null) {
120+
// There is no other query, return.
121+
return;
122+
}
123+
124+
_addInterval(fastestQueryId, fastestInterval);
125+
}
126+
127+
/// Adds a [queryId] to the [intervalQueries] for a specific [interval]
128+
/// and starts the timer if it doesn't exist.
129+
void _addInterval(String queryId, Duration interval) {
130+
final existingSet = intervalQueries[interval];
131+
if (existingSet != null) {
132+
existingSet.add(queryId);
133+
} else {
134+
intervalQueries[interval] = {queryId};
135+
_pollingTimers[interval] = Timer.periodic(
136+
interval, (Timer timer) => fetchQueriesOnInterval(timer, interval));
137+
}
138+
}
139+
140+
/// Returns the fastest query that matches the [request] or null if none exists.
141+
MapEntry<String, WatchQueryOptions<Object?>>? _fastestEntryForRequest(
142+
Request request) {
143+
return registeredQueries.entries
144+
// All existing queries mapping to the same request.
145+
.where((entry) =>
146+
entry.value.asRequest == request &&
147+
entry.value.pollInterval != null)
148+
// Ascending is default (shortest poll interval first)
149+
.sortedBy((entry) => entry.value.pollInterval!)
150+
.firstOrNull;
91151
}
92152
}

0 commit comments

Comments
 (0)