Skip to content
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,5 @@ Interview Questions
- [Task #14](src/interview/find-by-type) - _Find By Type_
- [Task #15](src/interview/group-numbers) - _Group Numbers_
- [Task #16](src/interview/digit-permutation) - _Digit Permutation_
- [Task #17](src/interview/fetch-urls-with-callback) - _Fetch Urls With Callback_
- [Task #17](src/interview/fetch-urls-with-callback) - _Fetch Urls With Callback_
- [Task #18](src/interview/fetch-with-auto-retry) - _Fetch With Auto Retry_
97 changes: 97 additions & 0 deletions src/interview/fetch-with-auto-retry/solution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect, vi } from 'vitest';
import { fetchWithAutoRetry } from './solution';

describe('Fetch With Auto Retry | Interview | Testcases', () => {
it('#1 Should resolve on the first successful attempt', async () => {
const fetcher = vi.fn().mockResolvedValue('SUCCESS');

const result = await fetchWithAutoRetry(fetcher, 2);

expect(result).toBe('SUCCESS');
expect(fetcher).toHaveBeenCalledTimes(1);
});

it('#2 Should retry after rejection and eventually resolve', async () => {
const fetcher = vi
.fn()
.mockRejectedValueOnce(new Error('fail-1'))
.mockRejectedValueOnce(new Error('fail-2'))
.mockResolvedValueOnce('SUCCESS');

const result = await fetchWithAutoRetry(fetcher, 2);

expect(result).toBe('SUCCESS');
expect(fetcher).toHaveBeenCalledTimes(3);
});

it('#3 Should make count + 1 attempts at most', async () => {
const fetcher = vi.fn().mockRejectedValue(new Error('fail'));

await expect(fetchWithAutoRetry(fetcher, 2)).rejects.toThrow('fail');

expect(fetcher).toHaveBeenCalledTimes(3);
});

it('#4 Should reject with the last error if all attempts fail', async () => {
const firstError = new Error('first');
const secondError = new Error('second');
const lastError = new Error('last');

const fetcher = vi
.fn()
.mockRejectedValueOnce(firstError)
.mockRejectedValueOnce(secondError)
.mockRejectedValueOnce(lastError);

await expect(fetchWithAutoRetry(fetcher, 2)).rejects.toBe(lastError);

expect(fetcher).toHaveBeenCalledTimes(3);
});

it('#5 Should not retry after a successful attempt', async () => {
const fetcher = vi
.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValueOnce('SUCCESS')
.mockResolvedValueOnce('SHOULD_NOT_BE_CALLED');

const result = await fetchWithAutoRetry(fetcher, 5);

expect(result).toBe('SUCCESS');
expect(fetcher).toHaveBeenCalledTimes(2);
});

it('#6 Should make only one attempt when count is 0', async () => {
const fetcher = vi.fn().mockResolvedValue('SUCCESS');

const result = await fetchWithAutoRetry(fetcher, 0);

expect(result).toBe('SUCCESS');
expect(fetcher).toHaveBeenCalledTimes(1);
});

it('#7 Should reject after one failed attempt when count is 0', async () => {
const error = new Error('fail');
const fetcher = vi.fn().mockRejectedValue(error);

await expect(fetchWithAutoRetry(fetcher, 0)).rejects.toBe(error);

expect(fetcher).toHaveBeenCalledTimes(1);
});

it('#8 Should return the value from the successful retry', async () => {
const fetcher = vi
.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValueOnce({ status: 'ok', value: 42 });

const result = await fetchWithAutoRetry(fetcher, 1);

expect(result).toEqual({
status: 'ok',
value: 42,
});

expect(fetcher).toHaveBeenCalledTimes(2);
});
});
44 changes: 44 additions & 0 deletions src/interview/fetch-with-auto-retry/solution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Executes an asynchronous operation and automatically retries it
* when the returned Promise is rejected.
*
* The first attempt is executed immediately. The `count` parameter
* specifies the number of additional retry attempts after the first one.
*
* If any attempt succeeds, its result is returned immediately and no
* further attempts are made. If all attempts fail, the returned Promise
* is rejected with the error from the last failed attempt.
*
* @param fetcher - An asynchronous function to execute.
* @param count - The number of additional retry attempts.
* @returns A Promise that resolves with the first successful result.
* @throws error from the last failed attempt if all attempts fail.
*
* @example
* let calls = 0;
*
* function fetcher() {
* calls++;
*
* if (calls < 3) {
* return Promise.reject(new Error('fail'));
* }
*
* return Promise.resolve('SUCCESS');
* }
*
* fetchWithAutoRetry(fetcher, 2).then((value) => {
* console.log(value); // SUCCESS
* console.log(calls); // 3
* });
*/
export const fetchWithAutoRetry = async (
fetcher: () => Promise<unknown>,
count: number,
): Promise<unknown> =>
fetcher().catch((error: unknown) => {
if (count === 0) {
return Promise.reject(error);
}
return fetchWithAutoRetry(fetcher, count - 1);
});
Loading