summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils/poll.js
blob: 6ec1bd206e60c4600eeef9a40fa0516f37bc1f8c (plain)
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
import httpStatusCodes, { successCodes } from './http_status';
import { normalizeHeaders } from './common_utils';

/**
 * Polling utility for handling realtime updates.
 * Requirements: Promise based HTTP client
 *
 * Service for promise based http client and method need to be provided as props
 *
 * @example
 * new Poll({
 *   resource: resource,
 *   method: 'name',
 *   data: {page: 1, scope: 'all'}, // optional
 *   successCallback: () => {},
 *   errorCallback: () => {},
 *   notificationCallback: () => {}, // optional
 * }).makeRequest();
 *
 * Usage in pipelines table with visibility lib:
 *
 * const poll = new Poll({
 *  resource: this.service,
 *  method: 'getPipelines',
 *  data: { page: pageNumber, scope },
 *  successCallback: this.successCallback,
 *  errorCallback: this.errorCallback,
 *  notificationCallback: this.updateLoading,
 * });
 *
 * if (!Visibility.hidden()) {
 *  poll.makeRequest();
 *  }
 *
 * Visibility.change(() => {
 *  if (!Visibility.hidden()) {
 *   poll.restart();
 *  } else {
 *   poll.stop();
 *  }
 * });
 *
 * 1. Checks for response and headers before start polling
 * 2. Interval is provided by `Poll-Interval` header.
 * 3. If `Poll-Interval` is -1, we stop polling
 * 4. If HTTP response is 200, we poll.
 * 5. If HTTP response is different from 200, we stop polling.
 *
 * @example
 * // With initial delay (for, for example, reducing unnecessary requests)
 *
 * const poll = new Poll({
 *  resource: this.service,
 *  method: 'fetchNotes',
 *  successCallback: () => {},
 *  errorCallback: () => {},
 * });
 *
 * // Performs the first request in 2.5s and then uses the `Poll-Interval` header.
 * poll.makeDelayedRequest(2500);
 *
 */
export default class Poll {
  constructor(options = {}) {
    this.options = options;
    this.options.data = options.data || {};
    this.options.notificationCallback =
      options.notificationCallback || function notificationCallback() {};

    this.intervalHeader = 'POLL-INTERVAL';
    this.timeoutID = null;
    this.canPoll = true;
  }

  checkConditions(response) {
    const headers = normalizeHeaders(response.headers);
    const pollInterval = parseInt(headers[this.intervalHeader], 10);
    if (pollInterval > 0 && successCodes.indexOf(response.status) !== -1 && this.canPoll) {
      if (this.timeoutID) {
        clearTimeout(this.timeoutID);
      }

      this.timeoutID = setTimeout(() => {
        this.makeRequest();
      }, pollInterval);
    }
    this.options.successCallback(response);
  }

  makeDelayedRequest(delay = 0) {
    // So we don't make our specs artificially slower
    this.timeoutID = setTimeout(
      () => this.makeRequest(),
      process.env.NODE_ENV !== 'test' ? delay : 1,
    );
  }

  makeRequest() {
    const { resource, method, data, errorCallback, notificationCallback } = this.options;

    // It's called everytime a new request is made. Useful to update the status.
    notificationCallback(true);

    return resource[method](data)
      .then((response) => {
        this.checkConditions(response);
        notificationCallback(false);
      })
      .catch((error) => {
        notificationCallback(false);
        if (error.status === httpStatusCodes.ABORTED) {
          return;
        }
        errorCallback(error);
      });
  }

  /**
   * Stops the polling recursive chain
   * and guarantees if the timeout is already running it won't make another request by
   * cancelling the previously established timeout.
   */
  stop() {
    this.canPoll = false;
    clearTimeout(this.timeoutID);
  }

  /**
   * Enables polling after it has been stopped
   */
  enable(options) {
    if (options && options.data) {
      this.options.data = options.data;
    }

    this.canPoll = true;

    if (options && options.response) {
      this.checkConditions(options.response);
    }
  }

  /**
   * Restarts polling after it has been stopped and makes a request
   */
  restart(options) {
    this.enable(options);
    this.makeRequest();
  }
}