Skip to content

Commit

Permalink
feat: introduce persistent cache for providers
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
szymonmaslowski committed Feb 6, 2025
1 parent 24061e8 commit d412ba9
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { Logger } from 'ts-log';
import omit from 'lodash/omit.js';
import uniq from 'lodash/uniq.js';
import type { Cache } from '@cardano-sdk/util';
import type { Responses } from '@blockfrost/blockfrost-js';
import type { Schemas } from '@blockfrost/blockfrost-js/lib/types/open-api';

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

type BlockfrostChainHistoryProviderDependencies = {
cache: Cache<Cardano.HydratedTx>;
client: BlockfrostClient;
networkInfoProvider: NetworkInfoProvider;
logger: Logger;
};

export class BlockfrostChainHistoryProvider extends BlockfrostProvider implements ChainHistoryProvider {
private readonly cache: Map<string, Cardano.HydratedTx> = new Map();
private readonly cache: Cache<Cardano.HydratedTx>;
private networkInfoProvider: NetworkInfoProvider;

constructor(client: BlockfrostClient, networkInfoProvider: NetworkInfoProvider, logger: Logger) {
constructor({ cache, client, networkInfoProvider, logger }: BlockfrostChainHistoryProviderDependencies) {
super(client, logger);
this.cache = cache;
this.networkInfoProvider = networkInfoProvider;
}

Expand Down Expand Up @@ -478,12 +487,12 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement
public async transactionsByHashes({ ids }: TransactionsByIdsArgs): Promise<Cardano.HydratedTx[]> {
return Promise.all(
ids.map(async (id) => {
if (this.cache.has(id)) {
return this.cache.get(id)!;
}
const cached = await this.cache.get(id);
if (cached) return cached;

try {
const fetchedTransaction = await this.fetchTransaction(id);
this.cache.set(id, fetchedTransaction);
void this.cache.set(id, fetchedTransaction);
return fetchedTransaction;
} catch (error) {
throw this.toProviderError(error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { BlockfrostProvider, BlockfrostToCore, fetchSequentially } from '../blockfrost';
import { BlockfrostClient, BlockfrostProvider, BlockfrostToCore, fetchSequentially } from '../blockfrost';
import { Cardano, Serialization, UtxoByAddressesArgs, UtxoProvider } from '@cardano-sdk/core';
import { Logger } from 'ts-log';
import type { Cache } from '@cardano-sdk/util';
import type { Responses } from '@blockfrost/blockfrost-js';

type BlockfrostUtxoProviderDependencies = {
client: BlockfrostClient;
cache: Cache<Cardano.Tx>;
logger: Logger;
};

export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoProvider {
private readonly cache: Map<string, Cardano.Tx> = new Map();
private readonly cache: Cache<Cardano.Tx>;

constructor({ cache, client, logger }: BlockfrostUtxoProviderDependencies) {
super(client, logger);
this.cache = cache;
}

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

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

this.cache.set(hash, result);
void this.cache.set(hash, result);
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Cardano, NetworkInfoProvider } from '@cardano-sdk/core';
import { Responses } from '@blockfrost/blockfrost-js';
import { dummyLogger as logger } from 'ts-log';
import { mockResponses } from '../util';
import type { Cache } from '@cardano-sdk/util';

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

Expand Down Expand Up @@ -370,7 +371,23 @@ describe('blockfrostChainHistoryProvider', () => {
}
])
} as unknown as NetworkInfoProvider;
provider = new BlockfrostChainHistoryProvider(client, networkInfoProvider, logger);
const cacheStorage = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cache: Cache<any> = {
async get(key) {
return cacheStorage.get(key);
},
async set(key, value) {
cacheStorage.set(key, value);
}
};

provider = new BlockfrostChainHistoryProvider({
cache,
client,
logger,
networkInfoProvider
});
mockResponses(request, [
[`txs/${txId1}/utxos`, txsUtxosResponse],
[`txs/${txId1}`, mockedTx1Response],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Cardano } from '@cardano-sdk/core';
import { Responses } from '@blockfrost/blockfrost-js';
import { logger } from '@cardano-sdk/util-dev';
import { mockResponses } from '../util';
import type { Cache } from '@cardano-sdk/util';
jest.mock('@blockfrost/blockfrost-js');

const generateUtxoResponseMock = (qty: number) =>
Expand Down Expand Up @@ -31,7 +32,22 @@ describe('blockfrostUtxoProvider', () => {
beforeEach(async () => {
request = jest.fn();
const client = { request } as unknown as BlockfrostClient;
provider = new BlockfrostUtxoProvider(client, logger);
const cacheStorage = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cache: Cache<any> = {
async get(key) {
return cacheStorage.get(key);
},
async set(key, value) {
cacheStorage.set(key, value);
}
};

provider = new BlockfrostUtxoProvider({
cache,
client,
logger
});
address = Cardano.PaymentAddress(
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp'
);
Expand Down Expand Up @@ -84,7 +100,12 @@ describe('blockfrostUtxoProvider', () => {
const txHash = '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5';

mockResponses(request, [
[`txs/${txHash}/cbor`, 'mockedCBORData'],
[
`txs/${txHash}/cbor`,
{
cbor: '84a5008182582038685cff32e65bbf5be53e5478b2c9f91c686a663394484df2205d4733d19ad501018182583900bdb17d476dfdaa0d06b970a19e260fc97b9622fa7b85fc51205a552b3781e8276be4ebdaff285d7a29a144a4cf681af786f441cd8a6eea041a062a3a78021a0002a019031a049dea66048183098200581c3781e8276be4ebdaff285d7a29a144a4cf681af786f441cd8a6eea048102a10082825820518ab6ccb82cf0db3893dc532af0eb27bdfd68d696811ff5acf16c47d4792ab3584018f37f5f94397c10ab44d6d382a0abc83807ab8039b8bff3b172b5875d928cec5022f02d027e4077253a79910126562988c306c34f14dca9d79be4e1c6de940f82582076f79de7b22ea72556735ba30b49a6b176e03641d088f43b64ec299400d971b7584046f4ce5ee2d14ab15c38aa74063bf25c9481ceaec59f9cf55bc25f6aae20a3c8761db8f7e9621c5f6b3f5628c574f738c3c997f83210effd5790d54419254a0ef5f6'
}
],
[`addresses/${address.toString()}/utxos?page=1&count=100`, generateUtxoResponseMock(1)],
[`addresses/${address.toString()}/utxos?page=2&count=100`, generateUtxoResponseMock(0)]
]);
Expand All @@ -101,7 +122,7 @@ describe('blockfrostUtxoProvider', () => {

expect(secondResponse).toEqual(firstResponse);

expect(request).not.toHaveBeenCalledWith(`txs/${txHash}/cbor`);
expect(request).not.toHaveBeenCalledWith(`txs/${txHash}/cbor`, undefined);
});
});
});
27 changes: 25 additions & 2 deletions packages/cardano-services/src/Program/programs/providerServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ const selectProviderImplementation = <T extends Provider>(
return selected;
};

const createProviderCache = () => {
const cache = new Map();
return {
async get(key: string) {
return cache.get(key);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async set(key: string, val: any) {
cache.set(key, val);
}
};
};

const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
const { args, pools, dnsResolver, genesisData, logger, node } = options;

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

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

const getBlockfrostUtxoProvider = () => new BlockfrostUtxoProvider(getBlockfrostClient(), logger);
const getBlockfrostUtxoProvider = () =>
new BlockfrostUtxoProvider({
cache: createProviderCache(),
client: getBlockfrostClient(),
logger
});

const getDbSyncUtxoProvider = withDbSyncProvider(
(dbPools, cardanoNode) =>
Expand Down Expand Up @@ -344,7 +362,12 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
return networkInfoProvider;
};
const getBlockfrostChainHistoryProvider = (nInfoProvider: NetworkInfoProvider | DbSyncNetworkInfoProvider) =>
new BlockfrostChainHistoryProvider(getBlockfrostClient(), nInfoProvider, logger);
new BlockfrostChainHistoryProvider({
cache: createProviderCache(),
client: getBlockfrostClient(),
logger,
networkInfoProvider: nInfoProvider
});

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

Expand Down
31 changes: 22 additions & 9 deletions packages/e2e/src/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ const getWsClient = async (logger: Logger) => {
return (wsClient = new CardanoWsClient({ chainHistoryProvider, logger }, { url: new URL(env.WS_PROVIDER_URL) }));
};

const createProviderCache = () => {
const cache = new Map();
return {
async get(key: string) {
return cache.get(key);
},
async set(key: string, val: any) {
cache.set(key, val);
}
};
};

// Asset providers

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

return new Promise(async (resolve) => {
resolve(
new BlockfrostChainHistoryProvider(
new BlockfrostClient(
new BlockfrostChainHistoryProvider({
cache: createProviderCache(),
client: new BlockfrostClient(
{ apiVersion: params.apiVersion, baseUrl: params.baseUrl, projectId: params.projectId },
{ rateLimiter: { schedule: (task) => task() } }
),
await networkInfoProviderFactory.create('blockfrost', params, logger),
logger
)
logger,
networkInfoProvider: await networkInfoProviderFactory.create('blockfrost', params, logger)
})
);
});
});
Expand Down Expand Up @@ -360,16 +373,16 @@ utxoProviderFactory.register(

utxoProviderFactory.register(BLOCKFROST_PROVIDER, async (params: any, logger) => {
if (params.baseUrl === undefined) throw new Error(`${BlockfrostUtxoProvider.name}: ${MISSING_URL_PARAM}`);

return new Promise(async (resolve) => {
resolve(
new BlockfrostUtxoProvider(
new BlockfrostClient(
new BlockfrostUtxoProvider({
cache: createProviderCache(),
client: new BlockfrostClient(
{ apiVersion: params.apiVersion, baseUrl: params.baseUrl, projectId: params.projectId },
{ rateLimiter: { schedule: (task) => task() } }
),
logger
)
})
);
});
});
Expand Down
5 changes: 5 additions & 0 deletions packages/util/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ type Impossible<K extends keyof any> = {
[P in K]: never;
};
export type NoExtraProperties<T, U> = U & Impossible<Exclude<keyof U, keyof T>>;

export type Cache<T> = {
get(key: string): Promise<T | undefined>;
set(key: string, value: T): Promise<void>;
};
1 change: 1 addition & 0 deletions packages/web-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './supplyDistributionTracker';
export * from './keyAgent';
export * from './walletManager';
export * as cip30 from './cip30';
export * from './storage';
1 change: 1 addition & 0 deletions packages/web-extension/src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './persistentCacheStorage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { makePersistentCacheStorageFactory } from './persistentCacheStorage';
import { storage } from 'webextension-polyfill';

export const createPersistentCacheStorage = makePersistentCacheStorageFactory(storage.local, () => new Map());

export type CreatePersistentCacheStorage = typeof createPersistentCacheStorage;
Loading

0 comments on commit d412ba9

Please sign in to comment.