Skip to content

Commit 2e5f8ae

Browse files
algolia-botHaroenv
andcommitted
feat(javascript): allow cache on POST (generated)
algolia/api-clients-automation#5675 Co-authored-by: Haroen Viaene <hello@haroen.me>
1 parent 4db5b15 commit 2e5f8ae

File tree

3 files changed

+133
-15
lines changed

3 files changed

+133
-15
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { beforeEach, describe, expect, test } from 'vitest';
2+
import { createMemoryCache, createNullCache } from '../../cache';
3+
import { createNullLogger } from '../../logger';
4+
import { createTransporter } from '../../transporter';
5+
import type { AlgoliaAgent } from '../../types';
6+
7+
describe('transporter cache', () => {
8+
let requestCount: number;
9+
beforeEach(() => {
10+
requestCount = 0;
11+
});
12+
13+
const algoliaAgent: AlgoliaAgent = {
14+
value: 'test',
15+
add: () => algoliaAgent,
16+
};
17+
18+
const transporter = createTransporter({
19+
hosts: [{ url: 'localhost', accept: 'readWrite', protocol: 'https' }],
20+
hostsCache: createNullCache(),
21+
baseHeaders: {},
22+
baseQueryParameters: {},
23+
algoliaAgent,
24+
logger: createNullLogger(),
25+
timeouts: {
26+
connect: 1000,
27+
read: 2000,
28+
write: 3000,
29+
},
30+
requester: {
31+
send: async () => {
32+
requestCount++;
33+
return {
34+
status: 200,
35+
content: JSON.stringify({ value: requestCount }),
36+
isTimedOut: false,
37+
};
38+
},
39+
},
40+
requestsCache: createMemoryCache(),
41+
responsesCache: createMemoryCache(),
42+
});
43+
44+
test('uses cache for cacheable requests', async () => {
45+
const firstResponse = await transporter.request<{ value: number }>(
46+
{ method: 'GET', path: '/test-1', queryParameters: {}, headers: {}, cacheable: true },
47+
{},
48+
);
49+
const secondResponse = await transporter.request<{ value: number }>(
50+
{ method: 'GET', path: '/test-1', queryParameters: {}, headers: {}, cacheable: true },
51+
{},
52+
);
53+
54+
// Should use cached response, so both values are the same and only 1 request made
55+
expect(firstResponse).toEqual({ value: 1 });
56+
expect(secondResponse).toEqual({ value: 1 });
57+
expect(requestCount).toBe(1);
58+
});
59+
60+
test('does not use cache for implicit non-cacheable requests', async () => {
61+
const firstResponse = await transporter.request<{ value: number }>(
62+
{ method: 'POST', path: '/test-2', queryParameters: {}, headers: {} },
63+
{},
64+
);
65+
const secondResponse = await transporter.request<{ value: number }>(
66+
{ method: 'POST', path: '/test-2', queryParameters: {}, headers: {} },
67+
{},
68+
);
69+
70+
// Should NOT use cache, so each request increments the counter
71+
expect(firstResponse).toEqual({ value: 1 });
72+
expect(secondResponse).toEqual({ value: 2 });
73+
expect(requestCount).toBe(2);
74+
});
75+
76+
test('does not use cache for explicit non-cacheable requests', async () => {
77+
const firstResponse = await transporter.request<{ value: number }>(
78+
{ method: 'POST', path: '/test-3', queryParameters: {}, headers: {}, cacheable: false },
79+
{},
80+
);
81+
const secondResponse = await transporter.request<{ value: number }>(
82+
{ method: 'POST', path: '/test-3', queryParameters: {}, headers: {}, cacheable: false },
83+
{},
84+
);
85+
86+
// Should NOT use cache, so each request increments the counter
87+
expect(firstResponse).toEqual({ value: 1 });
88+
expect(secondResponse).toEqual({ value: 2 });
89+
expect(requestCount).toBe(2);
90+
});
91+
92+
test('uses cache for POST requests marked as cacheable', async () => {
93+
const firstResponse = await transporter.request<{ value: number }>(
94+
{ method: 'POST', path: '/test-4', queryParameters: {}, headers: {}, cacheable: true },
95+
{},
96+
);
97+
const secondResponse = await transporter.request<{ value: number }>(
98+
{ method: 'POST', path: '/test-4', queryParameters: {}, headers: {}, cacheable: true },
99+
{},
100+
);
101+
102+
// Should use cached response, so both values are the same and only 1 request made
103+
expect(firstResponse).toEqual({ value: 1 });
104+
expect(secondResponse).toEqual({ value: 1 });
105+
expect(requestCount).toBe(1);
106+
});
107+
108+
test('accepts cacheable from request options', async () => {
109+
const firstResponse = await transporter.request<{ value: number }>(
110+
{ method: 'GET', path: '/test-5', queryParameters: {}, headers: {}, cacheable: false },
111+
{ cacheable: true },
112+
);
113+
const secondResponse = await transporter.request<{ value: number }>(
114+
{ method: 'GET', path: '/test-5', queryParameters: {}, headers: {}, cacheable: false },
115+
{ cacheable: true },
116+
);
117+
118+
// Should use cached response, so both values are the same and only 1 request made
119+
expect(firstResponse).toEqual({ value: 1 });
120+
expect(secondResponse).toEqual({ value: 1 });
121+
expect(requestCount).toBe(1);
122+
});
123+
});

packages/client-common/src/transporter/createTransporter.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function createTransporter({
7272
async function retryableRequest<TResponse>(
7373
request: Request,
7474
requestOptions: RequestOptions,
75-
isRead = true,
75+
isRead: boolean,
7676
): Promise<TResponse> {
7777
const stackTrace: StackFrame[] = [];
7878

@@ -211,28 +211,21 @@ export function createTransporter({
211211
}
212212

213213
function createRequest<TResponse>(request: Request, requestOptions: RequestOptions = {}): Promise<TResponse> {
214-
/**
215-
* A read request is either a `GET` request, or a request that we make
216-
* via the `read` transporter (e.g. `search`).
217-
*/
218-
const isRead = request.useReadTransporter || request.method === 'GET';
219-
if (!isRead) {
220-
/**
221-
* On write requests, no cache mechanisms are applied, and we
222-
* proxy the request immediately to the requester.
223-
*/
224-
return retryableRequest<TResponse>(request, requestOptions, isRead);
225-
}
226-
227214
const createRetryableRequest = (): Promise<TResponse> => {
228215
/**
229216
* Then, we prepare a function factory that contains the construction of
230217
* the retryable request. At this point, we may *not* perform the actual
231218
* request. But we want to have the function factory ready.
232219
*/
233-
return retryableRequest<TResponse>(request, requestOptions);
220+
return retryableRequest<TResponse>(request, requestOptions, isRead);
234221
};
235222

223+
/**
224+
* A read request is either a `GET` request, or a request that we make
225+
* via the `read` transporter (e.g. `search`).
226+
*/
227+
const isRead = request.useReadTransporter || request.method === 'GET';
228+
236229
/**
237230
* Once we have the function factory ready, we need to determine of the
238231
* request is "cacheable" - should be cached. Note that, once again,

packages/client-common/vitest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineConfig({
1010
'src/__tests__/cache/memory-cache.test.ts',
1111
'src/__tests__/create-iterable-promise.test.ts',
1212
'src/__tests__/logger/null-logger.test.ts',
13+
'src/__tests__/transporter/cache.test.ts',
1314
],
1415
name: 'node',
1516
environment: 'node',
@@ -23,6 +24,7 @@ export default defineConfig({
2324
'src/__tests__/cache/null-cache.test.ts',
2425
'src/__tests__/create-iterable-promise.test.ts',
2526
'src/__tests__/logger/null-logger.test.ts',
27+
'src/__tests__/transporter/cache.test.ts',
2628
],
2729
name: 'jsdom',
2830
environment: 'jsdom',

0 commit comments

Comments
 (0)