diff --git a/clients/algoliasearch-client-javascript/packages/client-common/index.ts b/clients/algoliasearch-client-javascript/packages/client-common/index.ts index 3438b7c572..4a9f799ff1 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/index.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/index.ts @@ -1,5 +1,6 @@ export * from './src/createAuth'; export * from './src/createEchoRequester'; +export * from './src/createRetryablePromise'; export * from './src/cache'; export * from './src/transporter'; export * from './src/createAlgoliaAgent'; diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/create-retryable-promise.test.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/create-retryable-promise.test.ts new file mode 100644 index 0000000000..c17a96c3ee --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/__tests__/create-retryable-promise.test.ts @@ -0,0 +1,86 @@ +import { createRetryablePromise } from '../createRetryablePromise'; + +describe('createRetryablePromise', () => { + it('resolves promise after some retries', async () => { + let calls = 0; + const promise = createRetryablePromise({ + func: () => { + return new Promise((resolve) => { + calls += 1; + resolve(`success #${calls}`); + }); + }, + validate: () => calls >= 3, + }); + + await expect(promise).resolves.toEqual('success #3'); + expect(calls).toBe(3); + }); + + it('gets the rejection of the given promise via reject', async () => { + let calls = 0; + + const promise = createRetryablePromise({ + func: () => { + return new Promise((resolve, reject) => { + calls += 1; + if (calls <= 3) { + resolve('okay'); + } else { + reject(new Error('nope')); + } + }); + }, + validate: () => false, + }); + + await expect(promise).rejects.toEqual( + expect.objectContaining({ message: 'nope' }) + ); + }); + + it('gets the rejection of the given promise via throw', async () => { + let calls = 0; + + const promise = createRetryablePromise({ + func: () => { + return new Promise((resolve) => { + calls += 1; + if (calls <= 3) { + resolve('okay'); + } else { + throw new Error('nope'); + } + }); + }, + validate: () => false, + }); + + await expect(promise).rejects.toEqual( + expect.objectContaining({ message: 'nope' }) + ); + }); + + it('gets the rejection when it exceeds the max trial number', async () => { + const MAX_TRIAL = 3; + let calls = 0; + + const promise = createRetryablePromise({ + func: () => { + return new Promise((resolve) => { + calls += 1; + resolve('okay'); + }); + }, + validate: () => false, + maxTrial: MAX_TRIAL, + }); + + await expect(promise).rejects.toEqual( + expect.objectContaining({ + message: 'The maximum number of trials exceeded. (3/3)', + }) + ); + expect(calls).toBe(MAX_TRIAL); + }); +}); diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/createRetryablePromise.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/createRetryablePromise.ts new file mode 100644 index 0000000000..73bb69e73f --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/createRetryablePromise.ts @@ -0,0 +1,48 @@ +import type { CreateRetryablePromiseOptions } from './types/CreateRetryablePromise'; + +/** + * Return a promise that retry a task until it meets the condition. + * + * @param createRetryablePromiseOptions - The createRetryablePromise options. + * @param createRetryablePromiseOptions.func - The function to run, which returns a promise. + * @param createRetryablePromiseOptions.validate - The validator function. It receives the resolved return of `func`. + * @param createRetryablePromiseOptions.maxTrial - The maximum number of trials. 10 by default. + * @param createRetryablePromiseOptions.timeout - The function to decide how long to wait between tries. + */ +export function createRetryablePromise({ + func, + validate, + maxTrial = 10, + timeout = (retryCount: number): number => Math.min(retryCount * 10, 1000), +}: CreateRetryablePromiseOptions): Promise { + let retryCount = 0; + const retry = (): Promise => { + return new Promise((resolve, reject) => { + func() + .then((response) => { + const isValid = validate(response); + if (isValid) { + resolve(response); + } else if (retryCount + 1 >= maxTrial) { + reject( + new Error( + `The maximum number of trials exceeded. (${ + retryCount + 1 + }/${maxTrial})` + ) + ); + } else { + retryCount += 1; + setTimeout(() => { + retry().then(resolve).catch(reject); + }, timeout(retryCount)); + } + }) + .catch((error) => { + reject(error); + }); + }); + }; + + return retry(); +} diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/types/CreateRetryablePromise.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/types/CreateRetryablePromise.ts new file mode 100644 index 0000000000..8397e72381 --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/types/CreateRetryablePromise.ts @@ -0,0 +1,18 @@ +export type CreateRetryablePromiseOptions = { + /** + * The function to run, which returns a promise. + */ + func: () => Promise; + /** + * The validator function. It receives the resolved return of `func`. + */ + validate: (response: TResponse) => boolean; + /** + * The maximum number of trials. 10 by default. + */ + maxTrial?: number; + /** + * The function to decide how long to wait between tries. + */ + timeout?: (retryCount: number) => number; +}; diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/types/index.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/types/index.ts index ecfac34168..db8181231b 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/src/types/index.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/types/index.ts @@ -1,5 +1,6 @@ export * from './Cache'; export * from './CreateClient'; +export * from './CreateRetryablePromise'; export * from './Host'; export * from './Requester'; export * from './Transporter'; diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaJavaScriptGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaJavaScriptGenerator.java index 0c6315662f..6fbe671d59 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaJavaScriptGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaJavaScriptGenerator.java @@ -61,6 +61,7 @@ private void setDefaultGeneratorOptions() { additionalProperties.put("capitalizedApiName", Utils.capitalize(apiName)); additionalProperties.put("algoliaAgent", Utils.capitalize(CLIENT)); additionalProperties.put("gitRepoId", "algoliasearch-client-javascript"); + additionalProperties.put("isSearchClient", CLIENT.equals("search")); } /** Provides an opportunity to inspect and modify operation data before the code is generated. */ diff --git a/templates/javascript/api-single.mustache b/templates/javascript/api-single.mustache index 2de72ab276..3ef4f80e2a 100644 --- a/templates/javascript/api-single.mustache +++ b/templates/javascript/api-single.mustache @@ -3,6 +3,9 @@ import { createTransporter, getAlgoliaAgent, shuffle, + {{#isSearchClient}} + createRetryablePromise, + {{/isSearchClient}} } from '@experimental-api-clients-automation/client-common'; import type { CreateClientOptions, @@ -11,6 +14,9 @@ import type { Request, RequestOptions, QueryParameters, + {{#isSearchClient}} + CreateRetryablePromiseOptions, + {{/isSearchClient}} } from '@experimental-api-clients-automation/client-common'; {{#imports}} @@ -104,6 +110,32 @@ export function create{{capitalizedApiName}}(options: CreateClientOptions{{#hasR return { addAlgoliaAgent, + {{#isSearchClient}} + /** + * Wait for a task to complete with `indexName` and `taskID`. + * + * @summary Wait for a task to complete. + * @param waitForTaskProps - The waitForTaskProps object. + * @param waitForTaskProps.indexName - The index in which to perform the request. + * @param waitForTaskProps.taskID - The unique identifier of the task to wait for. + */ + waitForTask({ + indexName, + taskID, + ...createRetryablePromiseOptions, + }: { + indexName: string; + taskID: number; + } & Omit, 'func' | 'validate'>): Promise { + return new Promise((resolve, reject) => { + createRetryablePromise({ + ...createRetryablePromiseOptions, + func: () => this.getTask({ indexName, taskID }), + validate: (response) => response.status === 'published', + }).then(() => resolve()).catch(reject); + }); + }, + {{/isSearchClient}} {{#operation}} /** {{#notes}}