Skip to content

Commit d0e8286

Browse files
committed
index tokens positions
1 parent dfd0407 commit d0e8286

3 files changed

Lines changed: 84 additions & 11 deletions

File tree

src/crons/cache.warmer/cache.warmer.service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { IndexerService } from "src/common/indexer/indexer.service";
2626
import { NftService } from "src/endpoints/nfts/nft.service";
2727
import { AccountQueryOptions } from "src/endpoints/accounts/entities/account.query.options";
2828
import { Account, TokenType } from "src/common/indexer/entities";
29-
import { TokenDetailed } from "src/endpoints/tokens/entities/token.detailed";
29+
import { ArrayIndexer } from "src/utils/array.indexer";
3030
import { DataApiService } from "src/common/data-api/data-api.service";
3131
import { BlockService } from "src/endpoints/blocks/block.service";
3232
import { PoolService } from "src/endpoints/pool/pool.service";
@@ -277,10 +277,9 @@ export class CacheWarmerService {
277277
async handleTokenAssetsExtraInfoInvalidations() {
278278
const assets = await this.assetsService.getAllTokenAssets();
279279
const allTokens = await this.tokenService.getAllTokens();
280-
const allTokensIndexed = allTokens.toRecord<TokenDetailed>(token => token.identifier);
281280

282281
for (const identifier of Object.keys(assets)) {
283-
const token = allTokensIndexed[identifier];
282+
const token = ArrayIndexer.getItemByKeyValue(allTokens, 'identifier', identifier);
284283
if (!token) {
285284
continue;
286285
}

src/endpoints/tokens/token.service.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { TokenLogo } from "./entities/token.logo";
2929
import { AssetsService } from "src/common/assets/assets.service";
3030
import { CacheInfo } from "src/utils/cache.info";
3131
import { TokenAssets } from "src/common/assets/entities/token.assets";
32+
import { ArrayIndexer } from "src/utils/array.indexer";
3233
import { TransactionFilter } from "../transactions/entities/transaction.filter";
3334
import { TransactionService } from "../transactions/transaction.service";
3435
import { MexTokenService } from "../mex/mex.token.service";
@@ -74,14 +75,13 @@ export class TokenService {
7475

7576
async isToken(identifier: string): Promise<boolean> {
7677
const tokens = await this.getAllTokens();
77-
const lowercaseIdentifier = identifier.toLowerCase();
78-
return tokens.find(x => x.identifier.toLowerCase() === lowercaseIdentifier) !== undefined;
78+
return ArrayIndexer.getItemByKeyValue(tokens, 'identifier', this.normalizeIdentifierCase(identifier)) !== undefined;
7979
}
8080

8181
async getToken(rawIdentifier: string, supplyOptions?: TokenSupplyOptions): Promise<TokenDetailed | undefined> {
8282
const tokens = await this.getAllTokens();
8383
const identifier = this.normalizeIdentifierCase(rawIdentifier);
84-
let token = tokens.find(x => x.identifier === identifier);
84+
let token = ArrayIndexer.getItemByKeyValue(tokens, 'identifier', identifier);
8585

8686
if (!TokenUtils.isToken(identifier)) {
8787
return undefined;
@@ -280,12 +280,10 @@ export class TokenService {
280280

281281
const allTokens = await this.getAllTokens();
282282

283-
const allTokensIndexed = allTokens.toRecord<TokenDetailed>(token => token.identifier);
284-
285283
const result: TokenWithBalance[] = [];
286284
for (const elasticToken of elasticTokens) {
287-
if (allTokensIndexed[elasticToken.token]) {
288-
const token = allTokensIndexed[elasticToken.token];
285+
const token = ArrayIndexer.getItemByKeyValue(allTokens, 'identifier', elasticToken.token);
286+
if (token) {
289287

290288
const tokenWithBalance: TokenWithBalance = {
291289
...token,
@@ -660,7 +658,7 @@ export class TokenService {
660658
const result: TokenWithRoles[] = [];
661659

662660
for (const item of tokenList) {
663-
const token = allTokens.find(x => x.identifier === item.identifier);
661+
const token = ArrayIndexer.getItemByKeyValue(allTokens, 'identifier', item.identifier);
664662
if (token) {
665663
this.applyTickerFromAssets(token);
666664

src/utils/array.indexer.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Utility class designed to provide O(1) lookups for arrays of objects.
3+
* It uses a WeakMap to cache the positions of elements based on a specific property.
4+
* The WeakMap ensures that once the array is garbage collected, its associated cache is also cleared, preventing memory leaks.
5+
*/
6+
export class ArrayIndexer {
7+
/**
8+
* The cache stores the array instance as the WeakMap key.
9+
* The value is an internal Map where:
10+
* - Key: The name of the property used for indexing (e.g., 'identifier').
11+
* - Value: A Record mapping the actual property values to their index in the array.
12+
*/
13+
private static readonly cache = new WeakMap<any[], Map<string, Record<string, number>>>();
14+
15+
/**
16+
* Retrieves or builds a dictionary (Record) of array indices based on a specified property.
17+
* * @param array The array instance to be indexed.
18+
* @param propertyNameForKey The property key of the objects inside the array used to build the index.
19+
* @returns A Record mapping the property values to their corresponding indices in the array.
20+
*/
21+
static getOrSetPositions<T>(array: T[], propertyNameForKey: keyof T): Record<string, number> {
22+
const propertyString = String(propertyNameForKey);
23+
24+
// 1. Check if we already have cache entries for this specific array instance
25+
let arrayRecords = this.cache.get(array);
26+
27+
if (arrayRecords) {
28+
// 2. Check if we already computed the index Record for this specific property
29+
const cachedRecord = arrayRecords.get(propertyString);
30+
if (cachedRecord) {
31+
return cachedRecord; // Cache HIT: Return the existing index map
32+
}
33+
} else {
34+
// Initialize the internal Map for this new array instance
35+
arrayRecords = new Map<string, Record<string, number>>();
36+
this.cache.set(array, arrayRecords);
37+
}
38+
39+
// 3. Cache MISS: Build the index Record by iterating through the array O(N)
40+
const record: Record<string, number> = {};
41+
42+
for (let index = 0; index < array.length; index++) {
43+
const element = array[index];
44+
const key = String(element[propertyNameForKey]);
45+
46+
// Map the property's stringified value to its position (index) in the array
47+
record[key] = index;
48+
}
49+
50+
// 4. Save the newly built Record into the cache map for future use
51+
arrayRecords.set(propertyString, record);
52+
53+
return record;
54+
}
55+
56+
/**
57+
* Quickly retrieves an item from the array using the specified property and its value.
58+
* Leverages the cached index Record to perform an O(1) lookup.
59+
* * @param array The array to search in.
60+
* @param propertyNameForKey The property used for matching.
61+
* @param searchedKeyValue The exact value of the property to find.
62+
* @returns The found element, or undefined if it doesn't exist.
63+
*/
64+
static getItemByKeyValue<T>(array: T[], propertyNameForKey: keyof T, searchedKeyValue: string | number): T | undefined {
65+
// Retrieve the cached index mapping for this array and property
66+
const index = this.getOrSetPositions(array, propertyNameForKey)[String(searchedKeyValue)];
67+
68+
// If the index doesn't exist in our map, the item is not in the array
69+
if (index === undefined) {
70+
return undefined;
71+
}
72+
73+
// Return the element directly from the array using the fast O(1) index lookup
74+
return array[index];
75+
}
76+
}

0 commit comments

Comments
 (0)