1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191 | 10x
10x
263x
263x
263x
263x
263x
263x
263x
263x
10x
47x
47x
10x
20x
20x
20x
16x
4x
10x
31x
1x
30x
1x
29x
29x
4x
29x
29x
10x
34x
10x
67x
67x
69x
23x
46x
26x
20x
20x
20x
20x
20x
67x
22x
22x
10x
30x
30x
30x
30x
2x
28x
28x
67x
10x
7x
7x
10x
| // The QueryScheduler is supposed to be a mechanism that schedules polling queries such that
// they are clustered into the time slots of the QueryBatcher and are batched together. It
// also makes sure that for a given polling query, if one instance of the query is inflight,
// another instance will not be fired until the query returns or times out. We do this because
// another query fires while one is already in flight, the data will stay in the "loading" state
// even after the first query has returned.
// At the moment, the QueryScheduler implements the one-polling-instance-at-a-time logic and
// adds queries to the QueryBatcher queue.
import { QueryManager } from '../core/QueryManager';
import { FetchType, QueryListener } from '../core/types';
import { ObservableQuery } from '../core/ObservableQuery';
import { WatchQueryOptions } from '../core/watchQueryOptions';
import { NetworkStatus } from '../core/networkStatus';
export class QueryScheduler {
// Map going from queryIds to query options that are in flight.
public inFlightQueries: { [queryId: string]: WatchQueryOptions } = {};
// Map going from query ids to the query options associated with those queries. Contains all of
// the queries, both in flight and not in flight.
public registeredQueries: { [queryId: string]: WatchQueryOptions } = {};
// Map going from polling interval with to the query ids that fire on that interval.
// These query ids are associated with a set of options in the this.registeredQueries.
public intervalQueries: { [interval: number]: string[] } = {};
// We use this instance to actually fire queries (i.e. send them to the batching
// mechanism).
public queryManager: QueryManager;
// Map going from polling interval widths to polling timers.
private pollingTimers: { [interval: number]: any } = {};
private ssrMode: boolean = false;
constructor({
queryManager,
ssrMode,
}: {
queryManager: QueryManager;
ssrMode: boolean;
}) {
this.queryManager = queryManager;
this.ssrMode = ssrMode;
}
public checkInFlight(queryId: string) {
const query = this.queryManager.queryStore.get(queryId);
return (
query &&
query.networkStatus !== NetworkStatus.ready &&
query.networkStatus !== NetworkStatus.error
);
}
public fetchQuery<T>(
queryId: string,
options: WatchQueryOptions,
fetchType: FetchType,
) {
return new Promise((resolve, reject) => {
this.queryManager
.fetchQuery<T>(queryId, options, fetchType)
.then(result => {
resolve(result);
})
.catch(error => {
reject(error);
});
});
}
public startPollingQuery<T>(
optiIons: WatchQueryOptions,
queryId: string,
listener?: QueryListener,
): string {
if (!options.pollInterval) {
throw new Error(
'Attempted to start a polling query without a polling interval.',
);
}
// Do not poll in SSR mode
if (this.ssrMode) return queryId;
this.registeredQueries[queryId] = options;
if (Ilistener) {
this.queryManager.addQueryListener(queryId, listener);
}
this.addQueryOnInterval<T>(queryId, options);
return queryId;
}
public stopPollingQuery(queryId: string) {
// Remove the query options from one of the registered queries.
// The polling function will then take care of not firing it anymore.
delete this.registeredQueries[queryId];
}
// Fires the all of the queries on a particular interval. Called on a setInterval.
public fetchQueriesOnInterval<T>(interval: number) {
// XXX this "filter" here is nasty, because it does two things at the same time.
// 1. remove queries that have stopped polling
// 2. call fetchQueries for queries that are polling and not in flight.
// TODO: refactor this to make it cleaner
this.intervalQueries[interval] = this.intervalQueries[
interval
].filter(queryId => {
// If queryOptions can't be found from registeredQueries, it means that this queryId
// is no longer registered and should be removed from the list of queries firing on this
// interval.
if (!this.registeredQueries.hasOwnProperty(queryId)) {
return false;
}
// Don't fire this instance of the polling query is one of the instances is already in
// flight.
if (this.checkInFlight(queryId)) {
return true;
}
const queryOptions = this.registeredQueries[queryId];
const pollingOptions = { ...queryOptions } as WatchQueryOptions;
pollingOptions.fetchPolicy = 'network-only';
this.fetchQuery<T>(queryId, pollingOptions, FetchType.poll);
return true;
});
if (this.intervalQueries[interval].length === 0) {
clearInterval(this.pollingTimers[interval]);
delete this.intervalQueries[interval];
}
}
// Adds a query on a particular interval to this.intervalQueries and then fires
// that query with all the other queries executing on that interval. Note that the query id
// and query options must have been added to this.registeredQueries before this function is called.
public addQueryOnInterval<T>(
queryId: string,
queryOptions: WatchQueryOptions,
) {
const interval = queryOptions.pollInterval;
if (!interval) {
throw new Error(
`A poll interval is required to start polling query with id '${queryId}'.`,
);
}
// If there are other queries on this interval, this query will just fire with those
// and we don't need to create a new timer.
if (
this.intervalQueries.hasOwnProperty(interval.toString()) &&
this.intervalQueries[interval].length > 0
) {
this.intervalQueries[interval].push(queryId);
} else {
this.intervalQueries[interval] = [queryId];
// set up the timer for the function that will handle this interval
this.pollingTimers[interval] = setInterval(() => {
this.fetchQueriesOnInterval<T>(interval);
}, interval);
}
}
// Used only for unit testing.
public registerPollingQuery<T>(
queryOptions: WatchQueryOptions,
): ObservableQuery<T> {
if (!queryOptions.pollInterval) {
throw new Error(
'Attempted to register a non-polling query with the scheduler.',
);
}
return new ObservableQuery<T>({
scheduler: this,
options: queryOptions,
});
}
}
|