Skip to content

Commit e39bf3d

Browse files
feat: introduce persistent cache for providers
- define a contract of the cache - enable chain history and utxo providers to use injected cache - create persistent cache storage - utilizing web extension storage - implementing the cache contract - having data envicting capability
1 parent 24061e8 commit e39bf3d

File tree

13 files changed

+469
-28
lines changed

13 files changed

+469
-28
lines changed

packages/cardano-services-client/src/ChainHistoryProvider/BlockfrostChainHistoryProvider.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { Logger } from 'ts-log';
2525
import omit from 'lodash/omit.js';
2626
import uniq from 'lodash/uniq.js';
27+
import type { Cache } from '@cardano-sdk/util';
2728
import type { Responses } from '@blockfrost/blockfrost-js';
2829
import type { Schemas } from '@blockfrost/blockfrost-js/lib/types/open-api';
2930

@@ -33,12 +34,20 @@ export const DB_MAX_SAFE_INTEGER = 2_147_483_647;
3334
type BlockfrostTx = Pick<Responses['address_transactions_content'][0], 'block_height' | 'tx_index'>;
3435
const compareTx = (a: BlockfrostTx, b: BlockfrostTx) => a.block_height - b.block_height || a.tx_index - b.tx_index;
3536

37+
type BlockfrostChainHistoryProviderDependencies = {
38+
cache: Cache<Cardano.HydratedTx>;
39+
client: BlockfrostClient;
40+
networkInfoProvider: NetworkInfoProvider;
41+
logger: Logger;
42+
};
43+
3644
export class BlockfrostChainHistoryProvider extends BlockfrostProvider implements ChainHistoryProvider {
37-
private readonly cache: Map<string, Cardano.HydratedTx> = new Map();
45+
private readonly cache: Cache<Cardano.HydratedTx>;
3846
private networkInfoProvider: NetworkInfoProvider;
3947

40-
constructor(client: BlockfrostClient, networkInfoProvider: NetworkInfoProvider, logger: Logger) {
48+
constructor({ cache, client, networkInfoProvider, logger }: BlockfrostChainHistoryProviderDependencies) {
4149
super(client, logger);
50+
this.cache = cache;
4251
this.networkInfoProvider = networkInfoProvider;
4352
}
4453

@@ -478,12 +487,12 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement
478487
public async transactionsByHashes({ ids }: TransactionsByIdsArgs): Promise<Cardano.HydratedTx[]> {
479488
return Promise.all(
480489
ids.map(async (id) => {
481-
if (this.cache.has(id)) {
482-
return this.cache.get(id)!;
483-
}
490+
const cached = await this.cache.get(id);
491+
if (cached) return cached;
492+
484493
try {
485494
const fetchedTransaction = await this.fetchTransaction(id);
486-
this.cache.set(id, fetchedTransaction);
495+
void this.cache.set(id, fetchedTransaction);
487496
return fetchedTransaction;
488497
} catch (error) {
489498
throw this.toProviderError(error);

packages/cardano-services-client/src/UtxoProvider/BlockfrostUtxoProvider.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1-
import { BlockfrostProvider, BlockfrostToCore, fetchSequentially } from '../blockfrost';
1+
import { BlockfrostClient, BlockfrostProvider, BlockfrostToCore, fetchSequentially } from '../blockfrost';
22
import { Cardano, Serialization, UtxoByAddressesArgs, UtxoProvider } from '@cardano-sdk/core';
3+
import { Logger } from 'ts-log';
4+
import type { Cache } from '@cardano-sdk/util';
35
import type { Responses } from '@blockfrost/blockfrost-js';
46

7+
type BlockfrostUtxoProviderDependencies = {
8+
client: BlockfrostClient;
9+
cache: Cache<Cardano.Tx>;
10+
logger: Logger;
11+
};
12+
513
export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoProvider {
6-
private readonly cache: Map<string, Cardano.Tx> = new Map();
14+
private readonly cache: Cache<Cardano.Tx>;
15+
16+
constructor({ cache, client, logger }: BlockfrostUtxoProviderDependencies) {
17+
super(client, logger);
18+
this.cache = cache;
19+
}
720

821
protected async fetchUtxos(addr: Cardano.PaymentAddress, paginationQueryString: string): Promise<Cardano.Utxo[]> {
922
const queryString = `addresses/${addr.toString()}/utxos?${paginationQueryString}`;
@@ -29,9 +42,8 @@ export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoPr
2942
});
3043
}
3144
protected async fetchDetailsFromCBOR(hash: string) {
32-
if (this.cache.has(hash)) {
33-
return this.cache.get(hash);
34-
}
45+
const cached = await this.cache.get(hash);
46+
if (cached) return cached;
3547

3648
const result = await this.fetchCBOR(hash)
3749
.then((cbor) => {
@@ -48,7 +60,7 @@ export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoPr
4860
return null;
4961
}
5062

51-
this.cache.set(hash, result);
63+
void this.cache.set(hash, result);
5264
return result;
5365
}
5466

packages/cardano-services-client/test/ChainHistoryProvider/BlockfrostChainHistoryProvider.test.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Cardano, NetworkInfoProvider } from '@cardano-sdk/core';
33
import { Responses } from '@blockfrost/blockfrost-js';
44
import { dummyLogger as logger } from 'ts-log';
55
import { mockResponses } from '../util';
6+
import type { Cache } from '@cardano-sdk/util';
67

78
jest.mock('@blockfrost/blockfrost-js');
89

@@ -370,7 +371,23 @@ describe('blockfrostChainHistoryProvider', () => {
370371
}
371372
])
372373
} as unknown as NetworkInfoProvider;
373-
provider = new BlockfrostChainHistoryProvider(client, networkInfoProvider, logger);
374+
const cacheStorage = new Map();
375+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
376+
const cache: Cache<any> = {
377+
async get(key) {
378+
return cacheStorage.get(key);
379+
},
380+
async set(key, value) {
381+
cacheStorage.set(key, value);
382+
}
383+
};
384+
385+
provider = new BlockfrostChainHistoryProvider({
386+
cache,
387+
client,
388+
logger,
389+
networkInfoProvider
390+
});
374391
mockResponses(request, [
375392
[`txs/${txId1}/utxos`, txsUtxosResponse],
376393
[`txs/${txId1}`, mockedTx1Response],

packages/cardano-services-client/test/Utxo/BlockfrostUtxoProvider.test.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Cardano } from '@cardano-sdk/core';
33
import { Responses } from '@blockfrost/blockfrost-js';
44
import { logger } from '@cardano-sdk/util-dev';
55
import { mockResponses } from '../util';
6+
import type { Cache } from '@cardano-sdk/util';
67
jest.mock('@blockfrost/blockfrost-js');
78

89
const generateUtxoResponseMock = (qty: number) =>
@@ -31,7 +32,22 @@ describe('blockfrostUtxoProvider', () => {
3132
beforeEach(async () => {
3233
request = jest.fn();
3334
const client = { request } as unknown as BlockfrostClient;
34-
provider = new BlockfrostUtxoProvider(client, logger);
35+
const cacheStorage = new Map();
36+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
37+
const cache: Cache<any> = {
38+
async get(key) {
39+
return cacheStorage.get(key);
40+
},
41+
async set(key, value) {
42+
cacheStorage.set(key, value);
43+
}
44+
};
45+
46+
provider = new BlockfrostUtxoProvider({
47+
cache,
48+
client,
49+
logger
50+
});
3551
address = Cardano.PaymentAddress(
3652
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp'
3753
);
@@ -84,7 +100,12 @@ describe('blockfrostUtxoProvider', () => {
84100
const txHash = '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5';
85101

86102
mockResponses(request, [
87-
[`txs/${txHash}/cbor`, 'mockedCBORData'],
103+
[
104+
`txs/${txHash}/cbor`,
105+
{
106+
cbor: '84a5008182582038685cff32e65bbf5be53e5478b2c9f91c686a663394484df2205d4733d19ad501018182583900bdb17d476dfdaa0d06b970a19e260fc97b9622fa7b85fc51205a552b3781e8276be4ebdaff285d7a29a144a4cf681af786f441cd8a6eea041a062a3a78021a0002a019031a049dea66048183098200581c3781e8276be4ebdaff285d7a29a144a4cf681af786f441cd8a6eea048102a10082825820518ab6ccb82cf0db3893dc532af0eb27bdfd68d696811ff5acf16c47d4792ab3584018f37f5f94397c10ab44d6d382a0abc83807ab8039b8bff3b172b5875d928cec5022f02d027e4077253a79910126562988c306c34f14dca9d79be4e1c6de940f82582076f79de7b22ea72556735ba30b49a6b176e03641d088f43b64ec299400d971b7584046f4ce5ee2d14ab15c38aa74063bf25c9481ceaec59f9cf55bc25f6aae20a3c8761db8f7e9621c5f6b3f5628c574f738c3c997f83210effd5790d54419254a0ef5f6'
107+
}
108+
],
88109
[`addresses/${address.toString()}/utxos?page=1&count=100`, generateUtxoResponseMock(1)],
89110
[`addresses/${address.toString()}/utxos?page=2&count=100`, generateUtxoResponseMock(0)]
90111
]);
@@ -101,7 +122,7 @@ describe('blockfrostUtxoProvider', () => {
101122

102123
expect(secondResponse).toEqual(firstResponse);
103124

104-
expect(request).not.toHaveBeenCalledWith(`txs/${txHash}/cbor`);
125+
expect(request).not.toHaveBeenCalledWith(`txs/${txHash}/cbor`, undefined);
105126
});
106127
});
107128
});

packages/cardano-services/src/Program/programs/providerServer.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ const selectProviderImplementation = <T extends Provider>(
127127
return selected;
128128
};
129129

130+
const createProviderCache = () => {
131+
const cache = new Map();
132+
return {
133+
async get(key: string) {
134+
return cache.get(key);
135+
},
136+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
137+
async set(key: string, val: any) {
138+
cache.set(key, val);
139+
}
140+
};
141+
};
142+
130143
const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
131144
const { args, pools, dnsResolver, genesisData, logger, node } = options;
132145

@@ -298,7 +311,12 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
298311

299312
const getBlockfrostAssetProvider = () => new BlockfrostAssetProvider(getBlockfrostClient(), logger);
300313

301-
const getBlockfrostUtxoProvider = () => new BlockfrostUtxoProvider(getBlockfrostClient(), logger);
314+
const getBlockfrostUtxoProvider = () =>
315+
new BlockfrostUtxoProvider({
316+
cache: createProviderCache(),
317+
client: getBlockfrostClient(),
318+
logger
319+
});
302320

303321
const getDbSyncUtxoProvider = withDbSyncProvider(
304322
(dbPools, cardanoNode) =>
@@ -344,7 +362,12 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
344362
return networkInfoProvider;
345363
};
346364
const getBlockfrostChainHistoryProvider = (nInfoProvider: NetworkInfoProvider | DbSyncNetworkInfoProvider) =>
347-
new BlockfrostChainHistoryProvider(getBlockfrostClient(), nInfoProvider, logger);
365+
new BlockfrostChainHistoryProvider({
366+
cache: createProviderCache(),
367+
client: getBlockfrostClient(),
368+
logger,
369+
networkInfoProvider: nInfoProvider
370+
});
348371

349372
const getBlockfrostRewardsProvider = () => new BlockfrostRewardsProvider(getBlockfrostClient(), logger);
350373

packages/e2e/src/factories.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ const getWsClient = async (logger: Logger) => {
128128
return (wsClient = new CardanoWsClient({ chainHistoryProvider, logger }, { url: new URL(env.WS_PROVIDER_URL) }));
129129
};
130130

131+
const createProviderCache = () => {
132+
const cache = new Map();
133+
return {
134+
async get(key: string) {
135+
return cache.get(key);
136+
},
137+
async set(key: string, val: any) {
138+
cache.set(key, val);
139+
}
140+
};
141+
};
142+
131143
// Asset providers
132144

133145
assetProviderFactory.register(HTTP_PROVIDER, async (params: any, logger: Logger): Promise<AssetProvider> => {
@@ -181,14 +193,15 @@ chainHistoryProviderFactory.register(BLOCKFROST_PROVIDER, async (params: any, lo
181193

182194
return new Promise(async (resolve) => {
183195
resolve(
184-
new BlockfrostChainHistoryProvider(
185-
new BlockfrostClient(
196+
new BlockfrostChainHistoryProvider({
197+
cache: createProviderCache(),
198+
client: new BlockfrostClient(
186199
{ apiVersion: params.apiVersion, baseUrl: params.baseUrl, projectId: params.projectId },
187200
{ rateLimiter: { schedule: (task) => task() } }
188201
),
189-
await networkInfoProviderFactory.create('blockfrost', params, logger),
190-
logger
191-
)
202+
logger,
203+
networkInfoProvider: await networkInfoProviderFactory.create('blockfrost', params, logger)
204+
})
192205
);
193206
});
194207
});
@@ -360,16 +373,16 @@ utxoProviderFactory.register(
360373

361374
utxoProviderFactory.register(BLOCKFROST_PROVIDER, async (params: any, logger) => {
362375
if (params.baseUrl === undefined) throw new Error(`${BlockfrostUtxoProvider.name}: ${MISSING_URL_PARAM}`);
363-
364376
return new Promise(async (resolve) => {
365377
resolve(
366-
new BlockfrostUtxoProvider(
367-
new BlockfrostClient(
378+
new BlockfrostUtxoProvider({
379+
cache: createProviderCache(),
380+
client: new BlockfrostClient(
368381
{ apiVersion: params.apiVersion, baseUrl: params.baseUrl, projectId: params.projectId },
369382
{ rateLimiter: { schedule: (task) => task() } }
370383
),
371384
logger
372-
)
385+
})
373386
);
374387
});
375388
});

packages/e2e/test/blockfrost/queryTransactions.test.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,29 @@ import { Cardano, ChainHistoryProvider } from '@cardano-sdk/core';
33
import { logger } from '@cardano-sdk/util-dev';
44
import { util } from '@cardano-sdk/cardano-services';
55

6+
const createProviderCache = () => {
7+
const cache = new Map();
8+
return {
9+
async get(key: string) {
10+
return cache.get(key);
11+
},
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13+
async set(key: string, val: any) {
14+
cache.set(key, val);
15+
}
16+
};
17+
};
18+
619
describe.only('BlockfrostChainHistoryProvider', () => {
720
let chainHistoryProvider: ChainHistoryProvider;
821
beforeAll(async () => {
922
const networkInfoProvider = new BlockfrostNetworkInfoProvider(util.getBlockfrostClient(), logger);
10-
chainHistoryProvider = new BlockfrostChainHistoryProvider(util.getBlockfrostClient(), networkInfoProvider, logger);
23+
chainHistoryProvider = new BlockfrostChainHistoryProvider({
24+
cache: createProviderCache(),
25+
client: util.getBlockfrostClient(),
26+
logger,
27+
networkInfoProvider
28+
});
1129
});
1230

1331
describe('transactionsByHashes', () => {

packages/util/src/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@ type Impossible<K extends keyof any> = {
2828
[P in K]: never;
2929
};
3030
export type NoExtraProperties<T, U> = U & Impossible<Exclude<keyof U, keyof T>>;
31+
32+
export type Cache<T> = {
33+
get(key: string): Promise<T | undefined>;
34+
set(key: string, value: T): Promise<void>;
35+
};

packages/web-extension/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './supplyDistributionTracker';
44
export * from './keyAgent';
55
export * from './walletManager';
66
export * as cip30 from './cip30';
7+
export * from './storage';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './persistentCacheStorage';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { CreatePersistentCacheStorage, createPersistentCacheStorage } from './persistentCacheStorage';

0 commit comments

Comments
 (0)