diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/transporter/cache.test.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/transporter/cache.test.ts new file mode 100644 index 00000000000..08113fa2191 --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/transporter/cache.test.ts @@ -0,0 +1,123 @@ +import { beforeEach, describe, expect, test } from 'vitest'; +import { createMemoryCache, createNullCache } from '../../cache'; +import { createNullLogger } from '../../logger'; +import { createTransporter } from '../../transporter'; +import type { AlgoliaAgent } from '../../types'; + +describe('transporter cache', () => { + let requestCount: number; + beforeEach(() => { + requestCount = 0; + }); + + const algoliaAgent: AlgoliaAgent = { + value: 'test', + add: () => algoliaAgent, + }; + + const transporter = createTransporter({ + hosts: [{ url: 'localhost', accept: 'readWrite', protocol: 'https' }], + hostsCache: createNullCache(), + baseHeaders: {}, + baseQueryParameters: {}, + algoliaAgent, + logger: createNullLogger(), + timeouts: { + connect: 1000, + read: 2000, + write: 3000, + }, + requester: { + send: async () => { + requestCount++; + return { + status: 200, + content: JSON.stringify({ value: requestCount }), + isTimedOut: false, + }; + }, + }, + requestsCache: createMemoryCache(), + responsesCache: createMemoryCache(), + }); + + test('uses cache for cacheable requests', async () => { + const firstResponse = await transporter.request<{ value: number }>( + { method: 'GET', path: '/test-1', queryParameters: {}, headers: {}, cacheable: true }, + {}, + ); + const secondResponse = await transporter.request<{ value: number }>( + { method: 'GET', path: '/test-1', queryParameters: {}, headers: {}, cacheable: true }, + {}, + ); + + // Should use cached response, so both values are the same and only 1 request made + expect(firstResponse).toEqual({ value: 1 }); + expect(secondResponse).toEqual({ value: 1 }); + expect(requestCount).toBe(1); + }); + + test('does not use cache for implicit non-cacheable requests', async () => { + const firstResponse = await transporter.request<{ value: number }>( + { method: 'POST', path: '/test-2', queryParameters: {}, headers: {} }, + {}, + ); + const secondResponse = await transporter.request<{ value: number }>( + { method: 'POST', path: '/test-2', queryParameters: {}, headers: {} }, + {}, + ); + + // Should NOT use cache, so each request increments the counter + expect(firstResponse).toEqual({ value: 1 }); + expect(secondResponse).toEqual({ value: 2 }); + expect(requestCount).toBe(2); + }); + + test('does not use cache for explicit non-cacheable requests', async () => { + const firstResponse = await transporter.request<{ value: number }>( + { method: 'POST', path: '/test-3', queryParameters: {}, headers: {}, cacheable: false }, + {}, + ); + const secondResponse = await transporter.request<{ value: number }>( + { method: 'POST', path: '/test-3', queryParameters: {}, headers: {}, cacheable: false }, + {}, + ); + + // Should NOT use cache, so each request increments the counter + expect(firstResponse).toEqual({ value: 1 }); + expect(secondResponse).toEqual({ value: 2 }); + expect(requestCount).toBe(2); + }); + + test('uses cache for POST requests marked as cacheable', async () => { + const firstResponse = await transporter.request<{ value: number }>( + { method: 'POST', path: '/test-4', queryParameters: {}, headers: {}, cacheable: true }, + {}, + ); + const secondResponse = await transporter.request<{ value: number }>( + { method: 'POST', path: '/test-4', queryParameters: {}, headers: {}, cacheable: true }, + {}, + ); + + // Should use cached response, so both values are the same and only 1 request made + expect(firstResponse).toEqual({ value: 1 }); + expect(secondResponse).toEqual({ value: 1 }); + expect(requestCount).toBe(1); + }); + + test('accepts cacheable from request options', async () => { + const firstResponse = await transporter.request<{ value: number }>( + { method: 'GET', path: '/test-5', queryParameters: {}, headers: {}, cacheable: false }, + { cacheable: true }, + ); + const secondResponse = await transporter.request<{ value: number }>( + { method: 'GET', path: '/test-5', queryParameters: {}, headers: {}, cacheable: false }, + { cacheable: true }, + ); + + // Should use cached response, so both values are the same and only 1 request made + expect(firstResponse).toEqual({ value: 1 }); + expect(secondResponse).toEqual({ value: 1 }); + expect(requestCount).toBe(1); + }); +}); diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts index 300e5f98333..6d459bb8f07 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts @@ -72,7 +72,7 @@ export function createTransporter({ async function retryableRequest( request: Request, requestOptions: RequestOptions, - isRead = true, + isRead: boolean, ): Promise { const stackTrace: StackFrame[] = []; @@ -211,28 +211,21 @@ export function createTransporter({ } function createRequest(request: Request, requestOptions: RequestOptions = {}): Promise { - /** - * A read request is either a `GET` request, or a request that we make - * via the `read` transporter (e.g. `search`). - */ - const isRead = request.useReadTransporter || request.method === 'GET'; - if (!isRead) { - /** - * On write requests, no cache mechanisms are applied, and we - * proxy the request immediately to the requester. - */ - return retryableRequest(request, requestOptions, isRead); - } - const createRetryableRequest = (): Promise => { /** * Then, we prepare a function factory that contains the construction of * the retryable request. At this point, we may *not* perform the actual * request. But we want to have the function factory ready. */ - return retryableRequest(request, requestOptions); + return retryableRequest(request, requestOptions, isRead); }; + /** + * A read request is either a `GET` request, or a request that we make + * via the `read` transporter (e.g. `search`). + */ + const isRead = request.useReadTransporter || request.method === 'GET'; + /** * Once we have the function factory ready, we need to determine of the * request is "cacheable" - should be cached. Note that, once again, diff --git a/clients/algoliasearch-client-javascript/packages/client-common/vitest.config.ts b/clients/algoliasearch-client-javascript/packages/client-common/vitest.config.ts index 44e20e38f94..bcfa5067dc9 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/vitest.config.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ 'src/__tests__/cache/memory-cache.test.ts', 'src/__tests__/create-iterable-promise.test.ts', 'src/__tests__/logger/null-logger.test.ts', + 'src/__tests__/transporter/cache.test.ts', ], name: 'node', environment: 'node', @@ -23,6 +24,7 @@ export default defineConfig({ 'src/__tests__/cache/null-cache.test.ts', 'src/__tests__/create-iterable-promise.test.ts', 'src/__tests__/logger/null-logger.test.ts', + 'src/__tests__/transporter/cache.test.ts', ], name: 'jsdom', environment: 'jsdom',