Skip to content

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

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

Merged
merged 14 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
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({
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