Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(javascript): add waitForTask in search client #510

Merged
merged 14 commits into from
May 19, 2022
Merged
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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<TResponse>({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we provide some docs here pls?

func,
eunjae-lee marked this conversation as resolved.
Show resolved Hide resolved
validate,
maxTrial = 10,
timeout = (retryCount: number): number => Math.min(retryCount * 10, 1000),
}: CreateRetryablePromiseOptions<TResponse>): Promise<TResponse> {
let retryCount = 0;
const retry = (): Promise<TResponse> => {
return new Promise<TResponse>((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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type CreateRetryablePromiseOptions<TResponse> = {
/**
* The function to run, which returns a promise.
*/
func: () => Promise<TResponse>;
/**
* 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;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './Cache';
export * from './CreateClient';
export * from './CreateRetryablePromise';
export * from './Host';
export * from './Requester';
export * from './Transporter';
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
32 changes: 32 additions & 0 deletions templates/javascript/api-single.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
createTransporter,
getAlgoliaAgent,
shuffle,
{{#isSearchClient}}
createRetryablePromise,
{{/isSearchClient}}
} from '@experimental-api-clients-automation/client-common';
import type {
CreateClientOptions,
Expand All @@ -11,6 +14,9 @@ import type {
Request,
RequestOptions,
QueryParameters,
{{#isSearchClient}}
CreateRetryablePromiseOptions,
{{/isSearchClient}}
} from '@experimental-api-clients-automation/client-common';

{{#imports}}
Expand Down Expand Up @@ -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({
shortcuts marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a task to the backlog to write tests for those methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need a test for waitForTask since it's a combination of getTask and createRetryablePromise and both of the methods have their own tests. We could, in the future, have integration tests with real APIs. WDYT?

indexName,
taskID,
...createRetryablePromiseOptions,
}: {
indexName: string;
taskID: number;
} & Omit<CreateRetryablePromiseOptions<GetTaskResponse>, 'func' | 'validate'>): Promise<void> {
return new Promise<void>((resolve, reject) => {
createRetryablePromise<GetTaskResponse>({
...createRetryablePromiseOptions,
func: () => this.getTask({ indexName, taskID }),
validate: (response) => response.status === 'published',
}).then(() => resolve()).catch(reject);
});
},
{{/isSearchClient}}
{{#operation}}
/**
{{#notes}}
Expand Down