summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/lib/account-data-cache.ts
blob: 51154c97920bab2b6e8cfa8fed2ad5057dac5959 (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
import log from 'electron-log';
import { AccountToken, IAccountData } from '../../shared/daemon-rpc-types';

export enum AccountFetchRetryAction {
  stop,
  retry,
}
interface IAccountFetchWatcher {
  onFinish: () => void;
  onError: (error: Error) => AccountFetchRetryAction;
}

// An account data cache that helps to throttle RPC requests to get_account_data and retain the
// cached value for 1 minute.
export default class AccountDataCache {
  private currentAccount?: AccountToken;
  private expiresAt?: Date;
  private fetchAttempt = 0;
  private fetchRetryTimeout?: NodeJS.Timeout;
  private watchers: IAccountFetchWatcher[] = [];

  constructor(
    private fetchHandler: (token: AccountToken) => Promise<IAccountData>,
    private updateHandler: (data?: IAccountData) => void,
  ) {}

  public fetch(accountToken: AccountToken, watcher?: IAccountFetchWatcher) {
    // invalidate cache if account token has changed
    if (accountToken !== this.currentAccount) {
      this.invalidate();
      this.currentAccount = accountToken;
    }

    // Only fetch is value has expired
    if (this.isExpired()) {
      if (watcher) {
        this.watchers.push(watcher);
      }

      this.performFetch(accountToken);
    } else if (watcher) {
      watcher.onFinish();
    }
  }

  public invalidate() {
    if (this.fetchRetryTimeout) {
      clearTimeout(this.fetchRetryTimeout);
      this.fetchRetryTimeout = undefined;
      this.fetchAttempt = 0;
    }

    this.expiresAt = undefined;
    this.updateHandler();
    this.notifyWatchers((watcher) => {
      watcher.onError(new Error('Cancelled'));
    });
  }

  private setValue(value: IAccountData) {
    this.expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration
    this.updateHandler(value);
    this.notifyWatchers((watcher) => watcher.onFinish());
  }

  private isExpired() {
    return !this.expiresAt || this.expiresAt < new Date();
  }

  private async performFetch(accountToken: AccountToken) {
    try {
      // it's possible for invalidate() to be called or for a fetch for a different account token
      // to start before this fetch completes, so checking if the current account token is the one
      // used is necessary below.
      const accountData = await this.fetchHandler(accountToken);

      if (this.currentAccount === accountToken) {
        this.setValue(accountData);
      }
    } catch (error) {
      if (this.currentAccount === accountToken) {
        this.handleFetchError(accountToken, error);
      }
    }
  }

  private handleFetchError(accountToken: AccountToken, error: any) {
    let shouldRetry = true;

    this.notifyWatchers((watcher) => {
      if (watcher.onError(error) === AccountFetchRetryAction.stop) {
        shouldRetry = false;
      }
    });

    if (shouldRetry) {
      this.scheduleRetry(accountToken);
    }
  }

  private scheduleRetry(accountToken: AccountToken) {
    this.fetchAttempt += 1;

    // tslint:disable-next-line
    const delay = Math.min(2048, 1 << (this.fetchAttempt + 2)) * 1000;

    log.warn(`Failed to fetch account data. Retrying in ${delay} ms`);

    this.fetchRetryTimeout = global.setTimeout(() => {
      this.fetchRetryTimeout = undefined;
      this.performFetch(accountToken);
    }, delay);
  }

  private notifyWatchers(notify: (watcher: IAccountFetchWatcher) => void) {
    this.watchers.splice(0).forEach(notify);
  }
}