summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--gui/src/main/account-data-cache.ts35
-rw-r--r--gui/src/main/index.ts15
-rw-r--r--gui/src/renderer/app.tsx4
-rw-r--r--gui/src/renderer/components/Account.tsx5
-rw-r--r--gui/src/renderer/components/Settings.tsx9
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx1
-rw-r--r--gui/src/renderer/containers/SettingsPage.tsx1
-rw-r--r--gui/src/shared/account-expiry.ts4
-rw-r--r--gui/src/shared/ipc-schema.ts1
-rw-r--r--gui/test/account-data-cache.spec.ts123
11 files changed, 146 insertions, 54 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6cccd59c0..ca79befb91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,8 @@ Line wrap the file at 100 chars. Th
- Fix deadlock that may occur when the API cannot be reached while entering the connecting state.
- Fix bug causing desktop app to log in if account number field was filled when removing account
history.
+- Fix lack of account expiry updates when using the app in unpinned mode and improve updating of
+ account expiry overall.
#### Linux
- Make offline monitor aware of routing table changes.
diff --git a/gui/src/main/account-data-cache.ts b/gui/src/main/account-data-cache.ts
index 8b81a29472..a79a934537 100644
--- a/gui/src/main/account-data-cache.ts
+++ b/gui/src/main/account-data-cache.ts
@@ -6,18 +6,21 @@ import consumePromise from '../shared/promise';
import { Scheduler } from '../shared/scheduler';
import { InvalidAccountError } from './errors';
-const EXPIRED_ACCOUNT_REFRESH_PERIOD = 60_000;
-
interface IAccountFetchWatcher {
onFinish: () => void;
onError: (error: Error) => void;
}
+// Account data is valid for 1 minute unless the account has expired.
+const ACCOUNT_DATA_VALIDITY_SECONDS = 60_000;
+// Account data is valid for 10 seconds if the account has expired.
+const ACCOUNT_DATA_EXPIRED_VALIDITY_SECONDS = 10_000;
+
// 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 validUntil?: Date;
private performingFetch = false;
private waitStrategy = new WaitStrategy();
private fetchRetryScheduler = new Scheduler();
@@ -36,7 +39,7 @@ export default class AccountDataCache {
}
// Only fetch if value has expired
- if (this.isExpired()) {
+ if (!this.isValid()) {
if (watcher) {
this.watchers.push(watcher);
}
@@ -59,7 +62,7 @@ export default class AccountDataCache {
this.waitStrategy.reset();
this.performingFetch = false;
- this.expiresAt = undefined;
+ this.validUntil = undefined;
this.updateHandler();
this.notifyWatchers((watcher) => {
watcher.onError(new Error('Cancelled'));
@@ -72,14 +75,22 @@ export default class AccountDataCache {
}
}
- private setValue(value: IAccountData) {
- this.expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration
- this.updateHandler(value);
+ private setValue(accountData: IAccountData) {
+ this.validUntil = this.getValidUntil(accountData);
+ this.updateHandler(accountData);
this.notifyWatchers((watcher) => watcher.onFinish());
}
- private isExpired() {
- return !this.expiresAt || this.expiresAt < new Date();
+ private isValid() {
+ return this.validUntil && this.validUntil > new Date();
+ }
+
+ private getValidUntil(accountData: IAccountData): Date {
+ if (hasExpired(accountData.expiry)) {
+ return new Date(Date.now() + ACCOUNT_DATA_EXPIRED_VALIDITY_SECONDS);
+ } else {
+ return new Date(Date.now() + ACCOUNT_DATA_VALIDITY_SECONDS);
+ }
}
private async performFetch(accountToken: AccountToken) {
@@ -113,9 +124,7 @@ export default class AccountDataCache {
const currentDate = new Date();
const oneMinuteBeforeExpiry = dateByAddingComponent(accountExpiry, DateComponent.minute, -1);
- if (hasExpired(accountExpiry)) {
- return EXPIRED_ACCOUNT_REFRESH_PERIOD;
- } else if (oneMinuteBeforeExpiry >= currentDate && closeToExpiry(accountExpiry)) {
+ if (oneMinuteBeforeExpiry >= currentDate && closeToExpiry(accountExpiry)) {
return oneMinuteBeforeExpiry.getTime() - currentDate.getTime();
} else {
return undefined;
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index a139434020..8b047cb6c3 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -15,7 +15,7 @@ import * as path from 'path';
import { sprintf } from 'sprintf-js';
import * as uuid from 'uuid';
import config from '../config.json';
-import { hasExpired } from '../shared/account-expiry';
+import { closeToExpiry, hasExpired } from '../shared/account-expiry';
import { IApplication } from '../shared/application-types';
import BridgeSettingsBuilder from '../shared/bridge-settings-builder';
import {
@@ -1049,14 +1049,20 @@ class ApplicationMain {
}
private registerWindowListener(windowController: WindowController) {
- windowController.window?.on('show', () => {
+ windowController.window?.on('focus', () => {
// cancel notifications when window appears
this.notificationController.cancelPendingNotifications();
- this.updateAccountData();
+ if (
+ !this.accountData ||
+ closeToExpiry(this.accountData.expiry, 4) ||
+ hasExpired(this.accountData.expiry)
+ ) {
+ this.updateAccountData();
+ }
});
- windowController.window?.on('hide', () => {
+ windowController.window?.on('blur', () => {
// ensure notification guard is reset
this.notificationController.resetTunnelStateAnnouncements();
});
@@ -1172,6 +1178,7 @@ class ApplicationMain {
return response;
});
+ IpcMainEventChannel.account.handleUpdateData(() => this.updateAccountData());
IpcMainEventChannel.accountHistory.handleClear(async () => {
await this.daemonRpc.clearAccountHistory();
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 677286c092..febcaf8bc8 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -318,6 +318,10 @@ export default class AppRenderer {
return IpcRendererEventChannel.account.submitVoucher(voucherCode);
}
+ public updateAccountData(): void {
+ IpcRendererEventChannel.account.updateData();
+ }
+
public async connectTunnel(): Promise<void> {
const state = this.tunnelState.state;
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index 18980ca1a9..02e2b9f081 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -31,9 +31,14 @@ interface IProps {
onLogout: () => void;
onClose: () => void;
onBuyMore: () => Promise<void>;
+ updateAccountData: () => void;
}
export default class Account extends React.Component<IProps> {
+ public componentDidMount() {
+ this.props.updateAccountData();
+ }
+
public render() {
return (
<ModalContainer>
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 56b8fd5494..1610f776f9 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { colors, links } from '../../config.json';
import { hasExpired, formatRemainingTime } from '../../shared/account-expiry';
import { messages } from '../../shared/gettext';
+import History from '../lib/history';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import * as Cell from './cell';
import { Layout } from './Layout';
@@ -42,9 +43,17 @@ export interface IProps {
onViewPreferences: () => void;
onViewAdvancedSettings: () => void;
onExternalLink: (url: string) => void;
+ updateAccountData: () => void;
+ history: History;
}
export default class Settings extends React.Component<IProps> {
+ public componentDidMount() {
+ if (this.props.history.action === 'PUSH') {
+ this.props.updateAccountData();
+ }
+ }
+
public render() {
const showLargeTitle = this.props.loginState.type !== 'ok';
diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx
index 985161c838..8751107c74 100644
--- a/gui/src/renderer/containers/AccountPage.tsx
+++ b/gui/src/renderer/containers/AccountPage.tsx
@@ -22,6 +22,7 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
props.history.pop();
},
onBuyMore: () => props.app.openLinkWithAuth(links.purchase),
+ updateAccountData: () => props.app.updateAccountData(),
};
};
diff --git a/gui/src/renderer/containers/SettingsPage.tsx b/gui/src/renderer/containers/SettingsPage.tsx
index 97ff917e7e..57ded8cd24 100644
--- a/gui/src/renderer/containers/SettingsPage.tsx
+++ b/gui/src/renderer/containers/SettingsPage.tsx
@@ -26,6 +26,7 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
onViewPreferences: () => props.history.push('/settings/preferences'),
onViewAdvancedSettings: () => props.history.push('/settings/advanced'),
onExternalLink: (url: string) => props.app.openUrl(url),
+ updateAccountData: () => props.app.updateAccountData(),
};
};
diff --git a/gui/src/shared/account-expiry.ts b/gui/src/shared/account-expiry.ts
index 7ca382efb2..81c45fec07 100644
--- a/gui/src/shared/account-expiry.ts
+++ b/gui/src/shared/account-expiry.ts
@@ -5,10 +5,10 @@ export function hasExpired(expiry: DateType): boolean {
return new Date(expiry).getTime() < Date.now();
}
-export function closeToExpiry(expiry: DateType): boolean {
+export function closeToExpiry(expiry: DateType, days = 3): boolean {
return (
!hasExpired(expiry) &&
- new Date(expiry) <= dateByAddingComponent(new Date(), DateComponent.day, 3)
+ new Date(expiry) <= dateByAddingComponent(new Date(), DateComponent.day, days)
);
}
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 5789bcc149..e2b3ebbed3 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -164,6 +164,7 @@ export const ipcSchema = {
logout: invoke<void, void>(),
getWwwAuthToken: invoke<void, string>(),
submitVoucher: invoke<string, VoucherResponse>(),
+ updateData: send<void>(),
},
accountHistory: {
'': notifyRenderer<AccountToken | undefined>(),
diff --git a/gui/test/account-data-cache.spec.ts b/gui/test/account-data-cache.spec.ts
index c482e16a5d..6ed10c6908 100644
--- a/gui/test/account-data-cache.spec.ts
+++ b/gui/test/account-data-cache.spec.ts
@@ -135,41 +135,6 @@ describe('IAccountData cache', () => {
});
});
- it('should refetch if account has expired', async () => {
- const expiredSpy = spy();
- const nonExpiredSpy = spy();
-
- const update = new Promise<void>((resolve, reject) => {
- let firstAttempt = true;
- const fetch = () => {
- if (firstAttempt) {
- expiredSpy();
- firstAttempt = false;
- setTimeout(() => clock.tick(60_000), 0);
- return Promise.resolve({
- expiry: new Date('1969-01-01').toISOString(),
- });
- } else {
- nonExpiredSpy();
- resolve();
- return Promise.resolve(dummyAccountData);
- }
- };
-
- const cache = new AccountDataCache(fetch, () => {});
-
- cache.fetch(dummyAccountToken, {
- onFinish: () => {},
- onError: (_error: Error) => reject(),
- });
- });
-
- return expect(update).to.eventually.be.fulfilled.then(() => {
- expect(expiredSpy).to.have.been.called.once;
- expect(nonExpiredSpy).to.have.been.called.once;
- });
- });
-
it('should clear scheduled retry if another fetch is performed', async () => {
const firstError = spy();
const secondSuccess = spy();
@@ -261,4 +226,92 @@ describe('IAccountData cache', () => {
return expect(update).to.eventually.be.fulfilled;
});
+
+ it('should invalidate after 60 seconds', async () => {
+ const fetchSpy = spy();
+ const expiry = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
+
+ const update = new Promise<void>((resolve, reject) => {
+ const cache = new AccountDataCache(
+ (_accountToken) => {
+ fetchSpy();
+ return Promise.resolve({ expiry });
+ },
+ () => {},
+ );
+
+ cache.fetch(dummyAccountToken, {
+ onFinish: async () => {
+ clock.tick(59_000);
+ // Timeout to let asynchronous tasks finish
+ await new Promise((resolve) => setTimeout(resolve));
+
+ cache.fetch(dummyAccountToken, {
+ onFinish: async () => {
+ clock.tick(1_000);
+ // Timeout to let asynchronous tasks finish
+ await new Promise((resolve) => setTimeout(resolve));
+
+ cache.fetch(dummyAccountToken, {
+ onFinish: async () => {
+ resolve();
+ },
+ onError: (_error: Error) => reject(),
+ });
+ },
+ onError: (_error: Error) => reject(),
+ });
+ },
+ onError: (_error: Error) => reject(),
+ });
+ });
+
+ return expect(update).to.eventually.be.fulfilled.then(() => {
+ expect(fetchSpy).to.have.been.called.twice;
+ });
+ });
+
+ it('should invalidate after 10 seconds when epired', async () => {
+ const fetchSpy = spy();
+ const expiry = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
+
+ const update = new Promise<void>((resolve, reject) => {
+ const cache = new AccountDataCache(
+ (_accountToken) => {
+ fetchSpy();
+ return Promise.resolve({ expiry });
+ },
+ () => {},
+ );
+
+ cache.fetch(dummyAccountToken, {
+ onFinish: async () => {
+ clock.tick(9_000);
+ // Timeout to let asynchronous tasks finish
+ await new Promise((resolve) => setTimeout(resolve));
+
+ cache.fetch(dummyAccountToken, {
+ onFinish: async () => {
+ clock.tick(1_000);
+ // Timeout to let asynchronous tasks finish
+ await new Promise((resolve) => setTimeout(resolve));
+
+ cache.fetch(dummyAccountToken, {
+ onFinish: async () => {
+ resolve();
+ },
+ onError: (_error: Error) => reject(),
+ });
+ },
+ onError: (_error: Error) => reject(),
+ });
+ },
+ onError: (_error: Error) => reject(),
+ });
+ });
+
+ return expect(update).to.eventually.be.fulfilled.then(() => {
+ expect(fetchSpy).to.have.been.called.twice;
+ });
+ });
});