Skip to content

Commit

Permalink
perf: improve token list performance by caching target tokens on load…
Browse files Browse the repository at this point in the history
… and config change
  • Loading branch information
Ikari-Shinji-re authored and yeager-eren committed Jul 21, 2024
1 parent 97643ae commit 3cc55ff
Show file tree
Hide file tree
Showing 11 changed files with 518 additions and 35 deletions.
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"module": "esnext",
// use Node's module resolution algorithm, instead of the legacy TS one
"target": "ES2015",
"moduleResolution": "node",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
Expand Down
9 changes: 7 additions & 2 deletions widget/embedded/src/containers/Wallets/Wallets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export const WidgetContext = createContext<WidgetContextInterface>({
});

function Main(props: PropsWithChildren<PropTypes>) {
const { updateConfig, updateSettings, fetch: fetchMeta } = useAppStore();
const {
updateConfig,
updateSettings,
fetch: fetchMeta,
fetchStatus,
} = useAppStore();
const blockchains = useAppStore().blockchains();
const { findToken } = useAppStore();
const config = useAppStore().config;
Expand Down Expand Up @@ -61,7 +66,7 @@ function Main(props: PropsWithChildren<PropTypes>) {
dappConfig: props.config,
};
}
}, [props.config]);
}, [props.config, fetchStatus]);

const evmBasedChainNames = blockchains
.filter(isEvmBlockchain)
Expand Down
35 changes: 35 additions & 0 deletions widget/embedded/src/services/cacheService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Token } from 'rango-sdk';

interface CacheServiceInterface<V> {
get<K extends keyof V>(key: K): V[K] | undefined;
set<K extends keyof V>(key: K, value: V[K]): void;
remove<K extends keyof V>(key: K): void;
clear(): void;
}

class CacheService<T> implements CacheServiceInterface<T> {
#cache: Map<string, T[keyof T]> = new Map();

get<K extends keyof T>(key: K): T[K] | undefined {
return this.#cache.get(key as string) as T[K];
}

set<K extends keyof T>(key: K, value: T[K]): void {
this.#cache.set(key as string, value);
}

remove<K extends keyof T>(key: K): void {
this.#cache.delete(key as string);
}

clear(): void {
this.#cache.clear();
}
}

export type CachedEntries = {
supportedSourceTokens: Token[];
supportedDestinationTokens: Token[];
};

export const cacheService = new CacheService<CachedEntries>();
40 changes: 39 additions & 1 deletion widget/embedded/src/store/slices/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { DataSlice } from './data';
import type { SettingsSlice } from './settings';
import type { WidgetConfig } from '../../types';
import type { StateCreatorWithInitialData } from '../app';

import { cacheService } from '../../services/cacheService';
import { matchTokensFromConfigWithMeta } from '../utils';

export const DEFAULT_CONFIG: WidgetConfig = {
apiKey: '',
title: undefined,
Expand Down Expand Up @@ -54,7 +58,7 @@ export interface ConfigSlice {

export const createConfigSlice: StateCreatorWithInitialData<
WidgetConfig,
ConfigSlice & SettingsSlice,
ConfigSlice & SettingsSlice & DataSlice,
ConfigSlice
> = (initialData, set, get) => {
return {
Expand Down Expand Up @@ -87,6 +91,40 @@ export const createConfigSlice: StateCreatorWithInitialData<
// Actions
updateConfig: (nextConfig: WidgetConfig) => {
const currentConfig = get().config;
const {
_tokensMapByTokenHash: tokensMapByTokenHash,
_tokensMapByBlockchainName: tokensMapByBlockchainName,
} = get();

const supportedSourceTokens = matchTokensFromConfigWithMeta({
type: 'source',
config: {
blockchains: nextConfig.from?.blockchains,
tokens: nextConfig.from?.tokens,
},
meta: {
tokensMapByBlockchainName,
tokensMapByTokenHash,
},
});

const supportedDestinationTokens = matchTokensFromConfigWithMeta({
type: 'destination',
config: {
blockchains: nextConfig.to?.blockchains,
tokens: nextConfig.to?.tokens,
},
meta: {
tokensMapByBlockchainName,
tokensMapByTokenHash,
},
});

cacheService.set('supportedSourceTokens', supportedSourceTokens);
cacheService.set(
'supportedDestinationTokens',
supportedDestinationTokens
);

set({
config: {
Expand Down
84 changes: 84 additions & 0 deletions widget/embedded/src/store/slices/data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { EvmBlockchainMeta, Token } from 'rango-sdk';

import { assert, beforeEach, describe, expect, test } from 'vitest';

import { cacheService } from '../../services/cacheService';
import {
createEvmBlockchain,
createInitialAppStore,
Expand All @@ -17,6 +18,7 @@ let customTokens: [Token, Token, Token];
let rangoBlockchain: EvmBlockchainMeta;

beforeEach(() => {
cacheService.clear();
rangoBlockchain = createEvmBlockchain();

customTokens = [
Expand Down Expand Up @@ -52,6 +54,10 @@ beforeEach(() => {
customTokens.forEach((token) => {
const tokenHash = createTokenHash(token);
initData._tokensMapByTokenHash.set(tokenHash, token);
if (!initData._tokensMapByBlockchainName[token.blockchain]) {
initData._tokensMapByBlockchainName[token.blockchain] = [];
}
initData._tokensMapByBlockchainName[token.blockchain].push(tokenHash);
});

const appStore = createAppStore();
Expand Down Expand Up @@ -351,3 +357,81 @@ describe('search in tokens', () => {
expect(secondResult).toBe('RNG');
});
});

describe('supported tokens from config', () => {
test('Should ensure tokens include only tokens from the config', () => {
const rangoToken = customTokens[0];
const djangoToken = customTokens[1];

const configTokens = [rangoToken, djangoToken];

appStoreState = updateAppStoreConfig(appStoreState, {
from: {
tokens: configTokens,
},
to: { tokens: configTokens },
});

let sourceTokens = appStoreState.tokens({
type: 'source',
});

let destinationTokens = appStoreState.tokens({
type: 'destination',
});

expect(configTokens).toMatchObject(sourceTokens);
expect(configTokens).toMatchObject(destinationTokens);

appStoreState = updateAppStoreConfig(appStoreState, {
from: {
blockchains: [rangoBlockchain.name],
tokens: {
[rangoBlockchain.name]: {
tokens: configTokens,
isExcluded: false,
},
},
},
to: {
blockchains: [rangoBlockchain.name],
tokens: {
[rangoBlockchain.name]: {
tokens: configTokens,
isExcluded: false,
},
},
},
});

sourceTokens = appStoreState.tokens({
type: 'source',
});

destinationTokens = appStoreState.tokens({
type: 'destination',
});

expect(sourceTokens).toMatchObject(sourceTokens);
expect(configTokens).toMatchObject(destinationTokens);
});

test('Check tokens calculation is caching', () => {
const rangoToken = customTokens[0];
const djangoToken = customTokens[1];

appStoreState = updateAppStoreConfig(appStoreState, {
from: {
tokens: [rangoToken, djangoToken],
},
});

expect(cacheService.get('supportedSourceTokens')?.length ?? 0).toBe(0);

appStoreState.tokens({
type: 'source',
});

expect(cacheService.get('supportedSourceTokens')?.length ?? 0).toBe(2);
});
});
68 changes: 44 additions & 24 deletions widget/embedded/src/store/slices/data.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// We keep all the received data from server in this slice

import type { ConfigSlice } from './config';
import type { Balance, TokenHash } from '../../types';
import type { CachedEntries } from '../../services/cacheService';
import type { Balance, Blockchain, TokenHash } from '../../types';
import type { Asset, BlockchainMeta, SwapperMeta, Token } from 'rango-sdk';
import type { StateCreator } from 'zustand';

import { cacheService } from '../../services/cacheService';
import { httpService as sdk } from '../../services/httpService';
import { compareWithSearchFor, containsText } from '../../utils/common';
import { isTokenExcludedInConfig } from '../../utils/configs';
import { createTokenHash, isTokenNative } from '../../utils/meta';
import { sortLiquiditySourcesByGroupTitle } from '../../utils/settings';
import { areTokensEqual, compareTokenBalance } from '../../utils/wallets';
import { matchTokensFromConfigWithMeta } from '../utils';

type BlockchainOptions = {
type?: 'source' | 'destination';
Expand All @@ -30,6 +32,7 @@ export type FetchStatus = 'loading' | 'success' | 'failed';
export interface DataSlice {
_blockchainsMapByName: Map<string, BlockchainMeta>;
_tokensMapByTokenHash: Map<TokenHash, Token>;
_tokensMapByBlockchainName: Record<Blockchain, TokenHash[]>;
_popularTokens: Token[];
_swappers: SwapperMeta[];
fetchStatus: FetchStatus;
Expand All @@ -48,8 +51,9 @@ export const createDataSlice: StateCreator<
DataSlice
> = (set, get) => ({
// State
_blockchainsMapByName: new Map<string, BlockchainMeta>(),
_tokensMapByTokenHash: new Map<TokenHash, Token>(),
_blockchainsMapByName: new Map(),
_tokensMapByTokenHash: new Map(),
_tokensMapByBlockchainName: {},
_popularTokens: [],
_swappers: [],
fetchStatus: 'loading',
Expand Down Expand Up @@ -84,31 +88,41 @@ export const createDataSlice: StateCreator<
return list;
},
tokens: (options) => {
const tokensMapByHashToken = get()._tokensMapByTokenHash;
const tokensFromState = Array.from(tokensMapByHashToken?.values() || []);
const { _tokensMapByTokenHash, _tokensMapByBlockchainName, config } = get();
const tokensFromState = Array.from(_tokensMapByTokenHash.values());
const blockchainsMapByName = get()._blockchainsMapByName;

if (!options || !options?.type) {
if (!options || !options.type) {
return tokensFromState;
}

const config = get().config;
const supportedTokensConfig =
(options.type === 'source' ? config.from?.tokens : config.to?.tokens) ??
{};
const configType = options.type === 'source' ? 'from' : 'to';
const cacheKey: keyof CachedEntries =
options.type === 'source'
? 'supportedSourceTokens'
: 'supportedDestinationTokens';

let supportedTokens = cacheService.get(cacheKey);
if (!supportedTokens) {
supportedTokens = matchTokensFromConfigWithMeta({
type: options.type,
config: {
blockchains: config[configType]?.blockchains,
tokens: config[configType]?.tokens,
},
meta: {
tokensMapByTokenHash: _tokensMapByTokenHash,
tokensMapByBlockchainName: _tokensMapByBlockchainName,
},
});
cacheService.set(cacheKey, supportedTokens);
}

const blockchains = get().blockchains({
type: options.type,
});

const list = tokensFromState
const list = supportedTokens
.filter((token) => {
if (
supportedTokensConfig &&
isTokenExcludedInConfig(token, supportedTokensConfig)
) {
return false;
}

// If a specific blockchain has passed, we only keep that blockchain's tokens.
if (!!options.blockchain && token.blockchain !== options.blockchain) {
return false;
Expand Down Expand Up @@ -261,17 +275,18 @@ export const createDataSlice: StateCreator<
const response = await sdk().getAllMetadata({
enableCentralizedSwappers,
});

set({ fetchStatus: 'success' });
const blockchainsMapByName: Map<string, BlockchainMeta> = new Map<
string,
BlockchainMeta
>();

const tokensMapByHashToken: Map<TokenHash, Token> = new Map<
const tokensMapByTokenHash: Map<TokenHash, Token> = new Map<
TokenHash,
Token
>();
const tokensMapByBlockchainName: DataSlice['_tokensMapByBlockchainName'] =
{};
const tokens: Token[] = [];
const popularTokens: Token[] = response.popularTokens;
const swappers: SwapperMeta[] = response.swappers;
Expand Down Expand Up @@ -299,12 +314,17 @@ export const createDataSlice: StateCreator<

tokens.forEach((token) => {
const tokenHash = createTokenHash(token);
tokensMapByHashToken.set(tokenHash, token);
if (!tokensMapByBlockchainName[token.blockchain]) {
tokensMapByBlockchainName[token.blockchain] = [];
}
tokensMapByTokenHash.set(tokenHash, token);
tokensMapByBlockchainName[token.blockchain].push(tokenHash);
});

set({
_blockchainsMapByName: blockchainsMapByName,
_tokensMapByTokenHash: tokensMapByHashToken,
_tokensMapByTokenHash: tokensMapByTokenHash,
_tokensMapByBlockchainName: tokensMapByBlockchainName,
_popularTokens: popularTokens,
_swappers: swappers,
});
Expand Down
Loading

0 comments on commit 3cc55ff

Please sign in to comment.