Skip to content

Commit 8abe969

Browse files
committed
refactor: extract caching logic into appStorage class
1 parent 3419682 commit 8abe969

File tree

7 files changed

+140
-133
lines changed

7 files changed

+140
-133
lines changed

app/src/__stories__/StoryWrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class StoryAppStorage {
3232
});
3333
setSession = () => undefined;
3434
getSession = () => '';
35+
getCached = () => Promise.resolve({});
3536
}
3637

3738
// Create a store that pulls data from the mock GRPC and doesn't use

app/src/__tests__/store/authStore.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { grpc } from '@improbable-eng/grpc-web';
22
import { waitFor } from '@testing-library/react';
3-
import AppStorage from 'util/appStorage';
43
import { lndListChannels } from 'util/tests/sampleData';
54
import { AuthStore, createStore, Store } from 'store';
65

76
const grpcMock = grpc as jest.Mocked<typeof grpc>;
8-
const appStorageMock = AppStorage as jest.Mock<AppStorage>;
97

108
describe('AuthStore', () => {
119
let rootStore: Store;
@@ -46,8 +44,9 @@ describe('AuthStore', () => {
4644
});
4745

4846
it('should load credentials from session storage', async () => {
49-
const getMock = appStorageMock.mock.instances[0].getSession as jest.Mock;
50-
getMock.mockReturnValue('test-creds');
47+
jest
48+
.spyOn(window.sessionStorage.__proto__, 'getItem')
49+
.mockReturnValueOnce('test-creds');
5150
await store.init();
5251
expect(store.credentials).toBe('test-creds');
5352
});
@@ -61,8 +60,9 @@ describe('AuthStore', () => {
6160
}
6261
return undefined as any;
6362
});
64-
const getMock = appStorageMock.mock.instances[0].getSession as jest.Mock;
65-
getMock.mockReturnValue('test-creds');
63+
jest
64+
.spyOn(window.sessionStorage.__proto__, 'getItem')
65+
.mockReturnValueOnce('test-creds');
6666
await store.init();
6767
expect(store.credentials).toBe('');
6868
});

app/src/__tests__/store/channelStore.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as LND from 'types/generated/lnd_pb';
33
import { grpc } from '@improbable-eng/grpc-web';
44
import { waitFor } from '@testing-library/react';
55
import Big from 'big.js';
6-
import AppStorage from 'util/appStorage';
76
import { BalanceMode } from 'util/constants';
87
import {
98
lndChannel,
@@ -17,7 +16,6 @@ import Channel from 'store/models/channel';
1716
import ChannelStore from 'store/stores/channelStore';
1817

1918
const grpcMock = grpc as jest.Mocked<typeof grpc>;
20-
const appStorageMock = AppStorage as jest.Mock<AppStorage>;
2119

2220
describe('ChannelStore', () => {
2321
let rootStore: Store;
@@ -140,13 +138,14 @@ describe('ChannelStore', () => {
140138

141139
it('should use cached aliases for channels', async () => {
142140
const cache = {
143-
lastUpdated: Date.now(),
144-
aliases: {
141+
expires: Date.now() + 60 * 1000,
142+
data: {
145143
[lndGetNodeInfo.node.pubKey]: lndGetNodeInfo.node.alias,
146144
},
147145
};
148-
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
149-
getMock.mockImplementationOnce(() => cache);
146+
jest
147+
.spyOn(window.sessionStorage.__proto__, 'getItem')
148+
.mockReturnValue(JSON.stringify(cache));
150149

151150
const channel = new Channel(rootStore, lndChannel);
152151
store.channels = observable.map({
@@ -174,13 +173,14 @@ describe('ChannelStore', () => {
174173
it('should use cached fee rates for channels', async () => {
175174
const rate = +Big(lndGetChanInfo.node1Policy.feeRateMilliMsat).div(1000000).mul(100);
176175
const cache = {
177-
lastUpdated: Date.now(),
178-
feeRates: {
176+
expires: Date.now() + 60 * 1000,
177+
data: {
179178
[lndGetChanInfo.channelId]: rate,
180179
},
181180
};
182-
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
183-
getMock.mockImplementationOnce(() => cache);
181+
jest
182+
.spyOn(window.sessionStorage.__proto__, 'getItem')
183+
.mockReturnValue(JSON.stringify(cache));
184184

185185
const channel = new Channel(rootStore, lndChannel);
186186
store.channels = observable.map({

app/src/__tests__/store/settingsStore.spec.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import AppStorage from 'util/appStorage';
21
import { BalanceMode, Unit } from 'util/constants';
32
import { createStore, SettingsStore } from 'store';
43

5-
const appStorageMock = AppStorage as jest.Mock<AppStorage>;
6-
74
describe('SettingsStore', () => {
85
let store: SettingsStore;
96

@@ -12,12 +9,13 @@ describe('SettingsStore', () => {
129
});
1310

1411
it('should load settings', async () => {
15-
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
16-
getMock.mockImplementation(() => ({
17-
sidebarVisible: false,
18-
unit: Unit.bits,
19-
balanceMode: BalanceMode.routing,
20-
}));
12+
jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValueOnce(
13+
JSON.stringify({
14+
sidebarVisible: false,
15+
unit: Unit.bits,
16+
balanceMode: BalanceMode.routing,
17+
}),
18+
);
2119

2220
store.load();
2321

@@ -27,9 +25,6 @@ describe('SettingsStore', () => {
2725
});
2826

2927
it('should do nothing if nothing is saved in storage', () => {
30-
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
31-
getMock.mockReturnValue(undefined as any);
32-
3328
store.load();
3429

3530
expect(store.sidebarVisible).toEqual(true);

app/src/setupTests.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import './i18n';
1010
// adds support for lottie-web animations in unit test env
1111
import 'jest-canvas-mock';
1212

13-
// don't use the real localStorage in unit tests
14-
jest.mock('util/appStorage');
13+
// mock localStorage & sessionStorage in unit tests
14+
jest.spyOn(window.localStorage.__proto__, 'setItem');
15+
jest.spyOn(window.localStorage.__proto__, 'getItem');
16+
jest.spyOn(window.sessionStorage.__proto__, 'setItem');
17+
jest.spyOn(window.sessionStorage.__proto__, 'getItem');
1518

1619
beforeEach(() => {
1720
jest.clearAllMocks();

app/src/store/stores/channelStore.ts

Lines changed: 41 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,6 @@ const {
2020
INACTIVE_CHANNEL,
2121
} = ChannelEventUpdate.UpdateType;
2222

23-
interface AliasCache {
24-
lastUpdated: number;
25-
/** mapping from remove pubkey to alias */
26-
aliases: Record<string, string>;
27-
}
28-
29-
interface FeeCache {
30-
lastUpdated: number;
31-
/** mapping form channel id to fee rate */
32-
feeRates: Record<string, number>;
33-
}
34-
35-
/** cache alias data for 24 hours */
36-
const CACHE_TIMEOUT = 24 * 60 * 60 * 1000;
37-
3823
export default class ChannelStore {
3924
private _store: Store;
4025

@@ -127,46 +112,22 @@ export default class ChannelStore {
127112
*/
128113
@action.bound
129114
async fetchAliases() {
130-
this._store.log.info('fetching aliases for channels');
131-
// create an array of all channel pubkeys
132-
let pubkeys = values(this.channels)
133-
.map(c => c.remotePubkey)
134-
.filter((r, i, a) => a.indexOf(r) === i); // remove duplicates
135-
136-
// create a map of pubkey to alias
137-
let aliases: Record<string, string> = {};
138-
139-
// look up cached data in storage
140-
let cachedAliases = this._store.storage.get<AliasCache>('aliases');
141-
if (cachedAliases && cachedAliases.lastUpdated > Date.now() - CACHE_TIMEOUT) {
142-
// there is cached data and it has not expired
143-
aliases = cachedAliases.aliases;
144-
// exclude pubkeys which we have aliases for already
145-
pubkeys = pubkeys.filter(pk => !aliases[pk]);
146-
this._store.log.info(`found aliases in cache. ${pubkeys.length} missing`, pubkeys);
147-
}
148-
149-
// if there are any pubkeys that we do not have a cached alias for
150-
if (pubkeys.length) {
151-
// call getNodeInfo for each pubkey and wait for all the requests to complete
152-
const nodeInfos = await Promise.all(
153-
pubkeys.map(pk => this._store.api.lnd.getNodeInfo(pk)),
154-
);
155-
156-
// add fetched aliases to the mapping
157-
aliases = nodeInfos.reduce((acc, { node }) => {
158-
if (node) acc[node.pubKey] = node.alias;
159-
return acc;
160-
}, aliases);
161-
162-
// save updated aliases to the cache in storage
163-
cachedAliases = {
164-
lastUpdated: Date.now(),
165-
aliases,
166-
};
167-
this._store.storage.set('aliases', cachedAliases);
168-
this._store.log.info(`updated cache with ${pubkeys.length} new aliases`);
169-
}
115+
const aliases = await this._store.storage.getCached<string>({
116+
cacheKey: 'aliases',
117+
requiredKeys: values(this.channels).map(c => c.remotePubkey),
118+
log: this._store.log,
119+
fetchFromApi: async (missingKeys, data) => {
120+
// call getNodeInfo for each pubkey and wait for all the requests to complete
121+
const nodeInfos = await Promise.all(
122+
missingKeys.map(id => this._store.api.lnd.getNodeInfo(id)),
123+
);
124+
// return a mapping from pubkey to alias
125+
return nodeInfos.reduce((acc, { node }) => {
126+
if (node) acc[node.pubKey] = node.alias;
127+
return acc;
128+
}, data);
129+
},
130+
});
170131

171132
runInAction('fetchAliasesContinuation', () => {
172133
// set the alias on each channel in the store
@@ -186,60 +147,37 @@ export default class ChannelStore {
186147
*/
187148
@action.bound
188149
async fetchFeeRates() {
189-
this._store.log.info('fetching fees for channels');
190-
// create an array of all channel ids
191-
let chanIds = values(this.channels)
192-
.map(c => c.chanId)
193-
.filter((r, i, a) => a.indexOf(r) === i); // remove duplicates
194-
195-
// create a map of chan id to fee rate
196-
let feeRates: Record<string, number> = {};
197-
198-
// look up cached data in storage
199-
let cachedFees = this._store.storage.get<FeeCache>('fee-rates');
200-
if (cachedFees && cachedFees.lastUpdated > Date.now() - CACHE_TIMEOUT) {
201-
// there is cached data and it has not expired
202-
feeRates = cachedFees.feeRates;
203-
// exclude chanIds which we have feeRates for already
204-
chanIds = chanIds.filter(id => !feeRates[id]);
205-
this._store.log.info(`found feeRates in cache. ${chanIds.length} missing`, chanIds);
206-
}
207-
208-
// if there are any chanIds that we do not have a cached fee rate for
209-
if (chanIds.length) {
210-
// call getNodeInfo for each chan id and wait for all the requests to complete
211-
const chanInfos = await Promise.all(
212-
chanIds.map(id => this._store.api.lnd.getChannelInfo(id)),
213-
);
214-
215-
// add fetched feeRates to the mapping
216-
feeRates = chanInfos.reduce((acc, info) => {
217-
const { channelId, node1Pub, node1Policy, node2Policy } = info;
218-
const localPubkey = this._store.nodeStore.pubkey;
219-
const policy = node1Pub === localPubkey ? node2Policy : node1Policy;
220-
if (policy) {
221-
acc[channelId] = +Big(policy.feeRateMilliMsat).div(1000000).mul(100);
222-
}
223-
return acc;
224-
}, feeRates);
225-
226-
// save updated feeRates to the cache in storage
227-
cachedFees = {
228-
lastUpdated: Date.now(),
229-
feeRates,
230-
};
231-
this._store.storage.set('fee-rates', cachedFees);
232-
this._store.log.info(`updated cache with ${chanIds.length} new feeRates`);
233-
}
150+
const feeRates = await this._store.storage.getCached<number>({
151+
cacheKey: 'feeRates',
152+
requiredKeys: values(this.channels).map(c => c.chanId),
153+
log: this._store.log,
154+
fetchFromApi: async (missingKeys, data) => {
155+
// call getNodeInfo for each pubkey and wait for all the requests to complete
156+
const chanInfos = await Promise.all(
157+
missingKeys.map(id => this._store.api.lnd.getChannelInfo(id)),
158+
);
159+
// return an updated mapping from chanId to fee rate
160+
return chanInfos.reduce((acc, info) => {
161+
const { channelId, node1Pub, node1Policy, node2Policy } = info;
162+
const localPubkey = this._store.nodeStore.pubkey;
163+
const policy = node1Pub === localPubkey ? node2Policy : node1Policy;
164+
if (policy) {
165+
acc[channelId] = +Big(policy.feeRateMilliMsat).div(1000000).mul(100);
166+
}
167+
return acc;
168+
}, data);
169+
},
170+
});
234171

235172
runInAction('fetchFeesContinuation', () => {
236173
// set the fee on each channel in the store
237174
values(this.channels).forEach(c => {
238-
if (feeRates[c.chanId]) {
239-
c.remoteFeeRate = feeRates[c.chanId];
175+
const rate = feeRates[c.chanId];
176+
if (rate) {
177+
c.remoteFeeRate = rate;
178+
this._store.log.info(`updated channel ${c.chanId} with remoteFeeRate ${rate}`);
240179
}
241180
});
242-
this._store.log.info('updated channels with feeRates', toJS(this.channels));
243181
});
244182
}
245183

0 commit comments

Comments
 (0)