aboutsummaryrefslogtreecommitdiffstats
path: root/src/stores/lib/CachedRequest.ts
blob: 3b15fcf3f35427eb3a8e09d85928e4adf659184d (plain) (blame)
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
import { isEqual } from 'lodash';
import { action } from 'mobx';
import Request from './Request';

export default class CachedRequest extends Request {
  _apiCalls: any[] = [];

  _isInvalidated = true;

  execute(...callArgs): this {
    // Do not continue if this request is already loading
    if (this.isWaitingForResponse) {
      return this;
    }

    // Very simple caching strategy -> only continue if the call / args changed
    // or the request was invalidated manually from outside
    const existingApiCall = this._findApiCall(callArgs);

    // Invalidate if new or different api call will be done
    if (existingApiCall && existingApiCall !== this.currentApiCall) {
      this._isInvalidated = true;
      this.currentApiCall = existingApiCall;
    } else if (!existingApiCall) {
      this._isInvalidated = true;
      this.currentApiCall = this._addApiCall(callArgs);
    }

    // Do not continue if this request is not invalidated (see above)
    if (!this._isInvalidated) {
      return this;
    }

    // This timeout is necessary to avoid warnings from mobx
    // regarding triggering actions as side-effect of getters
    setTimeout(
      action(() => {
        this.isExecuting = true;
        // Apply the previous result from this call immediately (cached)
        if (existingApiCall) {
          this.result = existingApiCall.result;
        }
      }),
      0,
    );

    // Issue api call & save it as promise that is handled to update the results of the operation
    this.promise = new Promise(resolve => {
      this.api[this.method](...callArgs)
        .then(result => {
          setTimeout(
            action(() => {
              this.result = result;
              if (this.currentApiCall) this.currentApiCall.result = result;
              this.isExecuting = false;
              this.isError = false;
              this.wasExecuted = true;
              this._isInvalidated = false;
              this.isWaitingForResponse = false;
              this._triggerHooks();
              resolve(result);
            }),
            1,
          );
          return result;
        })
        .catch(
          action(error => {
            setTimeout(
              action(() => {
                this.error = error;
                this.isExecuting = false;
                this.isError = true;
                this.wasExecuted = true;
                this.isWaitingForResponse = false;
                this._triggerHooks();
                // reject(error);
              }),
              1,
            );
          }),
        );
    });

    this.isWaitingForResponse = true;
    return this;
  }

  static defaultOptions = { immediately: false };

  invalidate(options = CachedRequest.defaultOptions): this {
    this._isInvalidated = true;
    if (options.immediately && this.currentApiCall) {
      return this.execute(...this.currentApiCall.args);
    }
    return this;
  }

  patch(modify): Promise<this> {
    return new Promise(resolve => {
      setTimeout(
        action(() => {
          const override = modify(this.result);
          if (override !== undefined) this.result = override;
          if (this.currentApiCall) this.currentApiCall.result = this.result;
          resolve(this);
        }),
        0,
      );
    });
  }

  _addApiCall(args: any) {
    const newCall = { args, result: null };
    this._apiCalls.push(newCall);
    return newCall;
  }

  _findApiCall(args: any) {
    return this._apiCalls.find(c => isEqual(c.args, args));
  }
}