Skip to content

Commit 755ed1b

Browse files
committed
channels: cache aliases to reduce API requests
1 parent 6f1ba90 commit 755ed1b

File tree

11 files changed

+101
-32
lines changed

11 files changed

+101
-32
lines changed

app/src/__stories__/LoopPage.stories.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
2+
import { observable, ObservableMap, values } from 'mobx';
23
import { useStore } from 'store';
4+
import { Channel } from 'store/models';
35
import { Layout } from 'components/layout';
46
import LoopPage from 'components/loop/LoopPage';
57

@@ -9,10 +11,20 @@ export default {
911
parameters: { contained: true },
1012
};
1113

14+
const channelSubset = (channels: ObservableMap<string, Channel>) => {
15+
const few = values(channels)
16+
.slice(0, 20)
17+
.reduce((result, c) => {
18+
result[c.chanId] = c;
19+
return result;
20+
}, {} as Record<string, Channel>);
21+
return observable.map(few);
22+
};
23+
1224
export const Default = () => {
13-
const store = useStore();
25+
const { channelStore } = useStore();
1426
// only use a small set of channels
15-
store.channelStore.sortedChannels.splice(10);
27+
channelStore.channels = channelSubset(channelStore.channels);
1628

1729
return <LoopPage />;
1830
};

app/src/__stories__/StoryWrapper.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { BalanceMode, Unit } from 'util/constants';
44
import { AuthenticationError } from 'util/errors';
55
import { sampleApiResponses } from 'util/tests/sampleData';
66
import { createStore, StoreProvider } from 'store';
7-
import { PersistentSettings } from 'store/stores/settingsStore';
87
import { Background } from 'components/base';
98
import { ThemeProvider } from 'components/theme';
109

@@ -26,7 +25,7 @@ const grpc = {
2625
// fake the AppStorage dependency so that settings aren't shared across stories
2726
class StoryAppStorage {
2827
set = () => undefined;
29-
get = (): PersistentSettings => ({
28+
get = (): any => ({
3029
sidebarVisible: true,
3130
unit: Unit.sats,
3231
balanceMode: BalanceMode.receive,

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { waitFor } from '@testing-library/react';
33
import AppStorage from 'util/appStorage';
44
import { lndListChannels } from 'util/tests/sampleData';
55
import { AuthStore, createStore, Store } from 'store';
6-
import { PersistentSettings } from 'store/stores/settingsStore';
76

87
const grpcMock = grpc as jest.Mocked<typeof grpc>;
9-
const appStorageMock = AppStorage as jest.Mock<AppStorage<PersistentSettings>>;
8+
const appStorageMock = AppStorage as jest.Mock<AppStorage>;
109

1110
describe('AuthStore', () => {
1211
let rootStore: Store;

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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';
67
import { BalanceMode } from 'util/constants';
78
import {
89
lndChannel,
@@ -15,6 +16,7 @@ import Channel from 'store/models/channel';
1516
import ChannelStore from 'store/stores/channelStore';
1617

1718
const grpcMock = grpc as jest.Mocked<typeof grpc>;
19+
const appStorageMock = AppStorage as jest.Mock<AppStorage>;
1820

1921
describe('ChannelStore', () => {
2022
let rootStore: Store;
@@ -135,6 +137,26 @@ describe('ChannelStore', () => {
135137
});
136138
});
137139

140+
it('should use cached aliases for channels', async () => {
141+
const cache = {
142+
lastUpdated: Date.now(),
143+
aliases: {
144+
[lndGetNodeInfo.node.pubKey]: lndGetNodeInfo.node.alias,
145+
},
146+
};
147+
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
148+
getMock.mockImplementationOnce(() => cache);
149+
150+
const channel = new Channel(rootStore, lndChannel);
151+
store.channels = observable.map({
152+
[channel.chanId]: channel,
153+
});
154+
155+
await store.fetchAliases();
156+
expect(channel.alias).toBe(lndGetNodeInfo.node.alias);
157+
expect(grpcMock.unary).not.toBeCalled();
158+
});
159+
138160
describe('onChannelEvent', () => {
139161
const {
140162
OPEN_CHANNEL,

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import AppStorage from 'util/appStorage';
22
import { BalanceMode, Unit } from 'util/constants';
33
import { createStore, SettingsStore } from 'store';
4-
import { PersistentSettings } from 'store/stores/settingsStore';
54

6-
const appStorageMock = AppStorage as jest.Mock<AppStorage<PersistentSettings>>;
5+
const appStorageMock = AppStorage as jest.Mock<AppStorage>;
76

87
describe('SettingsStore', () => {
98
let store: SettingsStore;

app/src/__tests__/util/appStorage.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('appStorage util', () => {
99
someString: 'abc',
1010
someBool: false,
1111
};
12-
const appStorage = new AppStorage<typeof settings>();
12+
const appStorage = new AppStorage();
1313

1414
it('should save an object to localStorage', () => {
1515
appStorage.set(key, settings);

app/src/store/models/channel.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ export default class Channel {
2828
}
2929

3030
/**
31-
* The alias or remotePubkey shortened to ~20 chars with ellipses inside
31+
* The alias or remotePubkey shortened to 12 chars with ellipses inside
3232
*/
3333
@computed get aliasLabel() {
34-
return this.alias || ellipseInside(this.remotePubkey);
34+
// if the node does not specify an alias, it is set to a substring of
35+
// the pubkey. we want to the display the ellipsed pubkey in this case
36+
// instead of the substring.
37+
return this.alias && !this.remotePubkey.includes(this.alias as string)
38+
? this.alias
39+
: ellipseInside(this.remotePubkey);
3540
}
3641

3742
/**

app/src/store/store.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
SwapStore,
1414
UiStore,
1515
} from './stores';
16-
import { PersistentSettings } from './stores/settingsStore';
1716

1817
/**
1918
* The store used to manage global app state
@@ -40,7 +39,7 @@ export class Store {
4039
log: Logger;
4140

4241
/** the wrapper class around persistent storage */
43-
storage: AppStorage<PersistentSettings>;
42+
storage: AppStorage;
4443

4544
/** the class to use for exporting lists of models to CSV */
4645
csv: CsvExporter;
@@ -54,7 +53,7 @@ export class Store {
5453
constructor(
5554
lnd: LndApi,
5655
loop: LoopApi,
57-
storage: AppStorage<PersistentSettings>,
56+
storage: AppStorage,
5857
csv: CsvExporter,
5958
log: Logger,
6059
) {
@@ -149,10 +148,7 @@ export class Store {
149148
* @param grpcClient an alternate GrpcClient to use instead of the default
150149
* @param appStorage an alternate AppStorage to use instead of the default
151150
*/
152-
export const createStore = (
153-
grpcClient?: GrpcClient,
154-
appStorage?: AppStorage<PersistentSettings>,
155-
) => {
151+
export const createStore = (grpcClient?: GrpcClient, appStorage?: AppStorage) => {
156152
const grpc = grpcClient || new GrpcClient();
157153
const storage = appStorage || new AppStorage();
158154
const lndApi = new LndApi(grpc);

app/src/store/stores/channelStore.ts

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

23+
interface AliasCache {
24+
lastUpdated: number;
25+
aliases: Record<string, string>;
26+
}
27+
28+
/** cache alias data for 24 hours */
29+
const ALIAS_CACHE_TIMEOUT = 24 * 60 * 60 * 1000;
30+
2331
export default class ChannelStore {
2432
private _store: Store;
2533

@@ -110,26 +118,55 @@ export default class ChannelStore {
110118
*/
111119
@action.bound
112120
async fetchAliases() {
113-
const pubKeys = values(this.channels)
121+
this._store.log.info('fetching aliases for channels');
122+
// create an array of all channel pubkeys
123+
let pubkeys = values(this.channels)
114124
.map(c => c.remotePubkey)
115125
.filter((r, i, a) => a.indexOf(r) === i); // remove duplicates
116-
// call getNodeInfo for each pubkey and wait for all the requests to complete
117-
const nodeInfos = await Promise.all(
118-
pubKeys.map(pk => this._store.api.lnd.getNodeInfo(pk)),
119-
);
120126

121-
// create a map of pubKey to alias
122-
const aliases = nodeInfos.reduce((acc, { node }) => {
123-
if (node) acc[node.pubKey] = node.alias;
124-
return acc;
125-
}, {} as Record<string, string>);
127+
// create a map of pubkey to alias
128+
let aliases: Record<string, string> = {};
129+
130+
// look up cached data in storage
131+
let cachedAliases = this._store.storage.get<AliasCache>('aliases');
132+
if (cachedAliases && cachedAliases.lastUpdated > Date.now() - ALIAS_CACHE_TIMEOUT) {
133+
// there is cached data and it has not expired
134+
aliases = cachedAliases.aliases;
135+
// exclude pubkeys which we have aliases for already
136+
pubkeys = pubkeys.filter(pk => !aliases[pk]);
137+
this._store.log.info(`found aliases in cache. ${pubkeys.length} missing`, pubkeys);
138+
}
139+
140+
// if there are any pubkeys that we do not have a cached alias for
141+
if (pubkeys.length) {
142+
// call getNodeInfo for each pubkey and wait for all the requests to complete
143+
const nodeInfos = await Promise.all(
144+
pubkeys.map(pk => this._store.api.lnd.getNodeInfo(pk)),
145+
);
146+
147+
// add fetched aliases to the mapping
148+
aliases = nodeInfos.reduce((acc, { node }) => {
149+
if (node) acc[node.pubKey] = node.alias;
150+
return acc;
151+
}, aliases);
152+
153+
// save updated aliases to the cache in storage
154+
cachedAliases = {
155+
lastUpdated: Date.now(),
156+
aliases,
157+
};
158+
this._store.storage.set('aliases', cachedAliases);
159+
this._store.log.info(`updated cache with ${pubkeys.length} new aliases`);
160+
}
126161

127162
runInAction('fetchAliasesContinuation', () => {
163+
// set the alias on each channel in the store
128164
values(this.channels).forEach(c => {
129165
if (aliases[c.remotePubkey]) {
130166
c.alias = aliases[c.remotePubkey];
131167
}
132168
});
169+
this._store.log.info('updated channels with aliases', toJS(this.channels));
133170
});
134171
}
135172

app/src/store/stores/settingsStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default class SettingsStore {
7676
@action.bound
7777
load() {
7878
this._store.log.info('loading settings from localStorage');
79-
const settings = this._store.storage.get('settings');
79+
const settings = this._store.storage.get<PersistentSettings>('settings');
8080
if (settings) {
8181
this.sidebarVisible = settings.sidebarVisible;
8282
this.unit = settings.unit;

0 commit comments

Comments
 (0)