All files / src/scheduler scheduler.ts

88.24% Statements 60/68
79.31% Branches 23/29
93.33% Functions 14/15
89.39% Lines 59/66
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 19110x                     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,
    });
  }
}