Skip to content

Commit 50387b3

Browse files
committed
feat: implement cache short circuits
1 parent 5140ff8 commit 50387b3

File tree

7 files changed

+276
-30
lines changed

7 files changed

+276
-30
lines changed

packages/client/src/actions/hubs.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
} from '@aave/graphql-next';
1212
import type { ResultAsync } from '@aave/types-next';
1313
import type { AaveClient } from '../AaveClient';
14-
import { type CurrencyQueryOptions, DEFAULT_QUERY_OPTIONS } from '../options';
14+
import {
15+
type CurrencyQueryOptions,
16+
DEFAULT_QUERY_OPTIONS,
17+
type RequestPolicyOptions,
18+
} from '../options';
1519

1620
/**
1721
* Fetches a specific hub by address and chain ID.
@@ -31,9 +35,13 @@ import { type CurrencyQueryOptions, DEFAULT_QUERY_OPTIONS } from '../options';
3135
export function hub(
3236
client: AaveClient,
3337
request: HubRequest,
34-
options: Required<CurrencyQueryOptions> = DEFAULT_QUERY_OPTIONS,
38+
options: CurrencyQueryOptions & RequestPolicyOptions = DEFAULT_QUERY_OPTIONS,
3539
): ResultAsync<Hub | null, UnexpectedError> {
36-
return client.query(HubQuery, { request, ...options });
40+
return client.query(
41+
HubQuery,
42+
{ request, currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency },
43+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
44+
);
3745
}
3846

3947
/**

packages/client/src/actions/user.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type { AaveClient } from '../AaveClient';
2929
import {
3030
type CurrencyQueryOptions,
3131
DEFAULT_QUERY_OPTIONS,
32+
type RequestPolicyOptions,
3233
type TimeWindowQueryOptions,
3334
} from '../options';
3435

@@ -226,9 +227,13 @@ export function userBalances(
226227
export function userHistory(
227228
client: AaveClient,
228229
request: UserHistoryRequest,
229-
options: Required<CurrencyQueryOptions> = DEFAULT_QUERY_OPTIONS,
230+
options: CurrencyQueryOptions & RequestPolicyOptions = DEFAULT_QUERY_OPTIONS,
230231
): ResultAsync<PaginatedUserHistoryResult, UnexpectedError> {
231-
return client.query(UserHistoryQuery, { request, ...options });
232+
return client.query(
233+
UserHistoryQuery,
234+
{ request, currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency },
235+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
236+
);
232237
}
233238

234239
/**

packages/client/src/cache.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { ReservesRequestFilter } from '@aave/graphql-next';
2+
import {
3+
assertOk,
4+
bigDecimal,
5+
chainId,
6+
evmAddress,
7+
never,
8+
nonNullable,
9+
} from '@aave/types-next';
10+
import { beforeAll, describe, expect, it } from 'vitest';
11+
import { hub, hubs, reserves, supply, userHistory } from './actions';
12+
import {
13+
client,
14+
createNewWallet,
15+
ETHEREUM_FORK_ID,
16+
ETHEREUM_WETH_ADDRESS,
17+
fundErc20Address,
18+
} from './test-utils';
19+
import { sendWith } from './viem';
20+
21+
const user = await createNewWallet().then(async (wallet) => {
22+
await fundErc20Address(evmAddress(wallet.account.address), {
23+
address: ETHEREUM_WETH_ADDRESS,
24+
amount: bigDecimal('100'),
25+
decimals: 18,
26+
});
27+
return wallet;
28+
});
29+
30+
describe('Given the Aave SDK normalized graph cache', () => {
31+
describe(`When fetching a single 'Hub'`, () => {
32+
it('Then it should leverage cached data whenever possible', async () => {
33+
const primed = await hubs(client, {
34+
query: {
35+
chainIds: [chainId(1)],
36+
},
37+
});
38+
assertOk(primed);
39+
40+
const result = await hub(
41+
client,
42+
{
43+
hub: nonNullable(primed.value[0]).address,
44+
chainId: nonNullable(primed.value[0]).chain.chainId,
45+
},
46+
{
47+
requestPolicy: 'cache-only',
48+
},
49+
);
50+
51+
assertOk(result);
52+
expect(result.value).toEqual(primed.value[0]);
53+
});
54+
});
55+
56+
describe.only('When fetching user history by tx hash', () => {
57+
beforeAll(async () => {
58+
const setup = await reserves(client, {
59+
query: {
60+
tokens: [
61+
{
62+
chainId: ETHEREUM_FORK_ID,
63+
address: ETHEREUM_WETH_ADDRESS,
64+
},
65+
],
66+
},
67+
filter: ReservesRequestFilter.Supply,
68+
})
69+
.map(
70+
(list) =>
71+
list.find((reserve) => reserve.canSupply) ??
72+
never('No reserve found to supply to for the token'),
73+
)
74+
.andThen((reserve) =>
75+
supply(client, {
76+
sender: evmAddress(user.account.address),
77+
reserve: {
78+
chainId: reserve.chain.chainId,
79+
reserveId: reserve.id,
80+
spoke: reserve.spoke.address,
81+
},
82+
amount: {
83+
erc20: {
84+
value: bigDecimal('0.1'),
85+
},
86+
},
87+
}),
88+
)
89+
.andThen(sendWith(user))
90+
.andThen(client.waitForTransaction);
91+
92+
assertOk(setup);
93+
});
94+
95+
it('Then it should leverage cached data whenever possible', async () => {
96+
const result = await userHistory(client, {
97+
user: evmAddress(user.account.address),
98+
filter: {
99+
chainIds: [ETHEREUM_FORK_ID],
100+
},
101+
}).andThen(({ items }) =>
102+
userHistory(
103+
client,
104+
{
105+
user: evmAddress(user.account.address),
106+
filter: {
107+
txHash: {
108+
txHash: items[0]?.txHash ?? never('Expected a tx hash'),
109+
chainId: ETHEREUM_FORK_ID,
110+
},
111+
},
112+
},
113+
{
114+
requestPolicy: 'cache-only',
115+
},
116+
),
117+
);
118+
119+
assertOk(result);
120+
console.log(result.value);
121+
});
122+
});
123+
});

packages/client/src/cache.ts

Lines changed: 113 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
import type {
2-
BorrowActivity,
3-
Chain,
4-
Erc20Token,
5-
Hub,
6-
HubAsset,
7-
LiquidatedActivity,
8-
NativeToken,
9-
RepayActivity,
10-
Reserve,
11-
ReserveInfo,
12-
Spoke,
13-
SupplyActivity,
14-
SwapActivity,
15-
SwapByIntent,
16-
SwapByIntentWithApprovalRequired,
17-
SwapByTransaction,
18-
UserPosition,
19-
WithdrawActivity,
1+
import {
2+
type BorrowActivity,
3+
type Chain,
4+
type Erc20Token,
5+
type Hub,
6+
type HubAsset,
7+
type HubQuery,
8+
isTxHashInputVariant,
9+
type LiquidatedActivity,
10+
type NativeToken,
11+
type RepayActivity,
12+
type Reserve,
13+
type ReserveInfo,
14+
type Spoke,
15+
type SupplyActivity,
16+
type SwapActivity,
17+
type SwapByIntent,
18+
type SwapByIntentWithApprovalRequired,
19+
type SwapByTransaction,
20+
type UserHistoryQuery,
21+
type UserPosition,
22+
type VariablesOf,
23+
type WithdrawActivity,
2024
} from '@aave/graphql-next';
2125
import introspectedSchema from '@aave/graphql-next/schema';
26+
import type { DateTime, TxHash } from '@aave/types-next';
2227
import {
2328
cacheExchange,
2429
type Resolver,
@@ -46,17 +51,101 @@ export const exchange = cacheExchange({
4651
// value: transformToBigInt,
4752
// nonce: transformToBigInt,
4853
// },
54+
55+
Query: {
56+
hub: (_, { request }: VariablesOf<typeof HubQuery>) => {
57+
return {
58+
__typename: 'Hub',
59+
address: request.hub,
60+
chain: {
61+
__typename: 'Chain',
62+
chainId: request.chainId,
63+
},
64+
assets: [],
65+
};
66+
},
67+
68+
userHistory: (
69+
_parent,
70+
args: VariablesOf<typeof UserHistoryQuery>,
71+
cache,
72+
) => {
73+
console.log('jerjere', args.request.filter);
74+
// Only optimize for txHash filter lookups
75+
if (!isTxHashInputVariant(args.request.filter)) {
76+
return cache.resolve('Query', 'userHistory', args);
77+
}
78+
79+
const { txHash, chainId } = args.request.filter.txHash;
80+
81+
// Collect all cached pages for Query.userHistory
82+
const matches = cache
83+
.inspectFields('Query')
84+
.filter((f) => f.fieldName === 'userHistory')
85+
.reduce((set, f) => {
86+
const pageRef = cache.resolve('Query', f.fieldKey) as string | null;
87+
if (!pageRef) return set;
88+
89+
const itemRefs = cache.resolve(pageRef, 'items') as string[] | null;
90+
if (!itemRefs) return set;
91+
92+
for (const ref of itemRefs) {
93+
set.add(ref);
94+
}
95+
return set;
96+
}, new Set<string>())
97+
.values()
98+
.toArray()
99+
.filter((ref) => {
100+
const itemTxHash = cache.resolve(ref, 'txHash') as TxHash;
101+
if (itemTxHash !== txHash) return false;
102+
103+
// Verify chainId if spoke.chain.chainId exists
104+
const spokeRef = cache.resolve(ref, 'spoke') as string | null;
105+
if (spokeRef) {
106+
const chainRef = cache.resolve(spokeRef, 'chain') as
107+
| string
108+
| null;
109+
const itemChainId = chainRef
110+
? (cache.resolve(chainRef, 'chainId') as number | undefined)
111+
: undefined;
112+
if (typeof itemChainId === 'number') {
113+
return itemChainId === chainId;
114+
}
115+
}
116+
return true;
117+
})
118+
.sort((a, b) => {
119+
const ta = cache.resolve(a, 'timestamp') as DateTime;
120+
const tb = cache.resolve(b, 'timestamp') as DateTime;
121+
return tb.localeCompare(ta); // desc
122+
});
123+
124+
if (matches.length === 0) return undefined;
125+
126+
return {
127+
__typename: 'PaginatedUserHistoryResult',
128+
items: matches,
129+
pageInfo: {
130+
__typename: 'PaginatedResultInfo',
131+
prev: null,
132+
next: null,
133+
},
134+
};
135+
},
136+
},
49137
},
50138
keys: {
51139
// Entitied with composite key
52-
Hub: (data: Hub) => `Hub:${data.address}/chain:${data.chain.chainId}`,
140+
Hub: (data: Hub) => `address=${data.address},chain=${data.chain.chainId}`,
53141
HubAsset: (data: HubAsset) =>
54-
`HubAsset:${data.assetId}/hub:${data.hub.address}/chain:${data.hub.chain.chainId}`,
142+
`assetId=${data.assetId},hub=${data.hub.address},chain=${data.hub.chain.chainId}`,
55143
Reserve: (data: Reserve) =>
56-
`Reserve:${data.id}/spoke:${data.spoke.address}/chain:${data.chain.chainId}`,
144+
`reserveId=${data.id},spoke=${data.spoke.address},chain=${data.chain.chainId}`,
57145
ReserveInfo: (data: ReserveInfo) =>
58-
`ReserveInfo:${data.id}/spoke:${data.spoke.address}/chain:${data.chain.chainId}`,
59-
Spoke: (data: Spoke) => `Spoke:${data.address}/chain:${data.chain.chainId}`,
146+
`reserveId=${data.id},spoke=${data.spoke.address},chain=${data.chain.chainId}`,
147+
Spoke: (data: Spoke) =>
148+
`address=${data.address},chain=${data.chain.chainId}`,
60149

61150
// Entities with id field as key
62151
BorrowActivity: (data: BorrowActivity) => data.id,

packages/client/src/options.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Currency, TimeWindow } from '@aave/graphql-next';
2+
import type { RequestPolicy } from '@urql/core';
23

34
export type CurrencyQueryOptions = {
45
/**
@@ -18,7 +19,17 @@ export type TimeWindowQueryOptions = {
1819
timeWindow?: TimeWindow;
1920
};
2021

22+
export type RequestPolicyOptions = {
23+
/**
24+
* The request policy to use.
25+
*
26+
* @defaultValue 'cache-and-network'
27+
*/
28+
requestPolicy?: RequestPolicy;
29+
};
30+
2131
export const DEFAULT_QUERY_OPTIONS = {
2232
currency: Currency.Usd,
2333
timeWindow: TimeWindow.LastDay,
34+
requestPolicy: 'cache-and-network',
2435
} as const;

packages/graphql/src/graphql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import type {
4040
} from './enums';
4141
import type { introspection } from './graphql-env';
4242

43-
export type { FragmentOf } from 'gql.tada';
43+
export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
4444

4545
export const graphql = initGraphQLTada<{
4646
disableMasking: true;

packages/graphql/src/inputs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ export type ReserveErc20AmountInputWithPermit = ReturnType<
4848
typeof graphql.scalar<'ReserveErc20AmountInputWithPermit'>
4949
>;
5050
export type TxHashInput = ReturnType<typeof graphql.scalar<'TxHashInput'>>;
51+
52+
/**
53+
* @internal
54+
*/
55+
export function isTxHashInputVariant<T>(
56+
input: T,
57+
): input is T & { txHash: TxHashInput } {
58+
return isObject(input) && 'txHash' in input && input.txHash != null;
59+
}
60+
5161
export type UserSpokeInput = ReturnType<
5262
typeof graphql.scalar<'UserSpokeInput'>
5363
>;

0 commit comments

Comments
 (0)