Skip to content

Commit e98baca

Browse files
authored
Merge pull request #184 from aave/cesare/aave-2779-sdk-support-for-batch-gql-queries-in-aavekit-react
feat: support for batching of GQL queries
2 parents 80ec342 + e38acfc commit e98baca

File tree

17 files changed

+609
-24
lines changed

17 files changed

+609
-24
lines changed

.changeset/gold-flies-sink.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@aave/client": minor
3+
"@aave/react": minor
4+
"@aave/core": minor
5+
---
6+
7+
**feat:** smart baching of GQL queries.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525
"test:client": "vitest --project client",
2626
"test:client:ethers": "vitest --project client packages/client/src/ethers.test.ts",
2727
"test:client:viem": "vitest --project client packages/client/src/viem.test.ts",
28+
"test:core": "vitest --project core",
2829
"test:react": "vitest --project react",
2930
"test:react:viem": "vitest --project react packages/react/src/viem/*.test.ts",
3031
"test:types": "vitest --project types",
31-
"test": "pnpm test:types && pnpm test:client && pnpm test:react"
32+
"test": "pnpm test:types && pnpm test:core && pnpm test:client && pnpm test:react"
3233
},
3334
"license": "MIT",
3435
"devDependencies": {

packages/client/src/AaveClient.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,9 @@ export class AaveClient extends GqlClient {
7777
): Promise<void> {
7878
await this.refreshWhere(async (op) => {
7979
if (op.query === document) {
80-
const result = await this.query(
81-
document,
82-
op.variables as TVariables,
83-
'cache-only',
84-
);
80+
const result = await this.query(document, op.variables as TVariables, {
81+
requestPolicy: 'cache-only',
82+
});
8583

8684
if (result.isErr()) {
8785
return false;

packages/client/src/actions/hubs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export function hub(
3939
return client.query(
4040
HubQuery,
4141
{ request, currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency },
42-
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
42+
{
43+
requestPolicy:
44+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
45+
},
4346
);
4447
}
4548

packages/client/src/actions/misc.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ export function hasProcessedKnownTransaction(
6666
return client.query(
6767
HasProcessedKnownTransactionQuery,
6868
{ request },
69-
'network-only',
69+
{
70+
requestPolicy: 'network-only', // alwats hit the network
71+
batch: false, // never batch, always run ASAP
72+
},
7073
);
7174
}
7275

packages/client/src/actions/protocol.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ export function asset(
5050
currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency,
5151
timeWindow: options.timeWindow ?? DEFAULT_QUERY_OPTIONS.timeWindow,
5252
},
53-
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
53+
{
54+
requestPolicy:
55+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
56+
},
5457
);
5558
}
5659

@@ -78,7 +81,10 @@ export function assetPriceHistory(
7881
return client.query(
7982
AssetPriceHistoryQuery,
8083
{ request },
81-
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
84+
{
85+
requestPolicy:
86+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
87+
},
8288
);
8389
}
8490

@@ -105,7 +111,10 @@ export function assetSupplyHistory(
105111
return client.query(
106112
AssetSupplyHistoryQuery,
107113
{ request },
108-
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
114+
{
115+
requestPolicy:
116+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
117+
},
109118
);
110119
}
111120

@@ -132,6 +141,9 @@ export function assetBorrowHistory(
132141
return client.query(
133142
AssetBorrowHistoryQuery,
134143
{ request },
135-
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
144+
{
145+
requestPolicy:
146+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
147+
},
136148
);
137149
}

packages/client/src/actions/transactions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ export function activities(
448448
return client.query(
449449
ActivitiesQuery,
450450
{ request, currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency },
451-
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
451+
{
452+
requestPolicy:
453+
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
454+
},
452455
);
453456
}

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"wonka": "^6.3.5"
4141
},
4242
"devDependencies": {
43+
"msw": "^2.10.5",
4344
"tsup": "^8.5.0",
4445
"typescript": "^5.9.2"
4546
},
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { assertOk, ResultAsync } from '@aave/types';
2+
import { gql, stringifyDocument } from '@urql/core';
3+
import * as msw from 'msw';
4+
import { setupServer } from 'msw/node';
5+
import {
6+
afterAll,
7+
afterEach,
8+
beforeAll,
9+
beforeEach,
10+
describe,
11+
expect,
12+
it,
13+
} from 'vitest';
14+
15+
import type { Context } from './context';
16+
import { GqlClient } from './GqlClient';
17+
import { delay } from './utils';
18+
19+
// see: https://mswjs.io/docs/graphql/mocking-responses/query-batching
20+
function batched(url: string, handlers: Array<msw.RequestHandler>) {
21+
return msw.http.post(url, async ({ request }) => {
22+
const requestClone = request.clone();
23+
const payload = await request.clone().json();
24+
25+
// Ignore non-batched GraphQL queries.
26+
if (!Array.isArray(payload)) {
27+
return;
28+
}
29+
30+
const responses = await Promise.all(
31+
payload.map(async (query) => {
32+
// Construct an individual query request
33+
// to the same URL but with an unwrapped query body.
34+
const queryRequest = new Request(requestClone, {
35+
body: JSON.stringify(query),
36+
});
37+
38+
// Resolve the individual query request
39+
// against the list of request handlers you provide.
40+
const response = await msw.getResponse(handlers, queryRequest);
41+
42+
// Return the mocked response, if found.
43+
// Otherwise, perform the individual query as-is,
44+
// so it can be resolved against an original server.
45+
return response || fetch(msw.bypass(queryRequest));
46+
}),
47+
);
48+
49+
// Read the mocked response JSON bodies to use
50+
// in the response to the entire batched query.
51+
const queryData = await Promise.all(
52+
responses.map((response) => response?.json()),
53+
);
54+
55+
return msw.HttpResponse.json(queryData);
56+
});
57+
}
58+
59+
export const TestQuery = gql`
60+
query TestQuery($id: Int!) {
61+
value(id: $id)
62+
}
63+
`;
64+
65+
const context: Context = {
66+
displayName: 'GqlClient',
67+
environment: {
68+
name: 'test',
69+
backend: 'https://api.aave.com',
70+
indexingTimeout: 1000,
71+
pollingInterval: 1000,
72+
},
73+
headers: {},
74+
cache: null,
75+
debug: false,
76+
fragments: [],
77+
};
78+
79+
const api = msw.graphql.link(context.environment.backend);
80+
81+
const server = setupServer(
82+
batched(context.environment.backend, [
83+
api.query(TestQuery, ({ variables }) =>
84+
msw.HttpResponse.json({
85+
data: {
86+
value: variables.id,
87+
},
88+
}),
89+
),
90+
]),
91+
api.query(TestQuery, ({ variables }) =>
92+
msw.HttpResponse.json({
93+
data: {
94+
value: variables.id,
95+
},
96+
}),
97+
),
98+
);
99+
100+
describe(`Given an instance of the ${GqlClient.name}`, () => {
101+
let requests: Request[];
102+
103+
beforeAll(() => {
104+
server.events.on('request:start', ({ request }) => {
105+
requests.push(request.clone());
106+
});
107+
108+
server.listen();
109+
});
110+
111+
beforeEach(() => {
112+
requests = [];
113+
});
114+
115+
afterEach(() => {
116+
server.resetHandlers();
117+
});
118+
119+
afterAll(() => {
120+
server.close();
121+
});
122+
123+
describe('When executing a single isolated query', () => {
124+
it('Then it should execute it in its own HTTP request', async () => {
125+
const client = new GqlClient(context);
126+
127+
const result = await client.query(TestQuery, { id: 1 });
128+
129+
assertOk(result);
130+
expect(await requests[0]!.json()).toEqual({
131+
operationName: 'TestQuery',
132+
query: stringifyDocument(TestQuery),
133+
variables: { id: 1 },
134+
});
135+
});
136+
});
137+
138+
describe('When executing concurrent queries', () => {
139+
it('Then it should batch queries fired in the same event-loop tick into a single HTTP request', async () => {
140+
const client = new GqlClient(context);
141+
142+
const resul = await ResultAsync.combine([
143+
client.query(TestQuery, { id: 1 }),
144+
client.query(TestQuery, { id: 2 }),
145+
// on a separate tick, fire the third query
146+
ResultAsync.fromSafePromise(delay(1)).andThen(() =>
147+
client.query(TestQuery, { id: 3 }),
148+
),
149+
]);
150+
151+
assertOk(resul);
152+
expect(requests).toHaveLength(2); // 2 HTTP requests were made
153+
});
154+
});
155+
156+
describe('When batching concurrent queries', () => {
157+
it('Then it should limit batching to a maximum of 10 queries', async () => {
158+
const client = new GqlClient(context);
159+
160+
const result = await ResultAsync.combine(
161+
Array.from({ length: 15 }, (_, i) =>
162+
client.query(TestQuery, { id: String(i + 1) }),
163+
),
164+
);
165+
166+
assertOk(result);
167+
expect(requests).toHaveLength(2); // 2 HTTP requests were made
168+
});
169+
170+
it('Then it should allow single queries to skip the batching', async () => {
171+
const client = new GqlClient(context);
172+
173+
const result = await ResultAsync.combine([
174+
client.query(TestQuery, { id: 1 }),
175+
client.query(TestQuery, { id: 2 }),
176+
client.query(TestQuery, { id: 3 }, { batch: false }),
177+
]);
178+
179+
assertOk(result);
180+
expect(requests).toHaveLength(2); // 2 HTTP requests were made (one batched, one not)
181+
});
182+
});
183+
});

0 commit comments

Comments
 (0)