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 @@ -135,4 +135,5 @@ Interview Questions
- [Task #13](src/interview/build-comments-tree) - _Build Comments Tree_
- [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 #16](src/interview/digit-permutation) - _Digit Permutation_
- [Task #17](src/interview/fetch-urls-with-callback) - _Fetch Urls With Callback_
132 changes: 132 additions & 0 deletions src/interview/fetch-urls-with-callback/solution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { fetchUrlsWithCallback } from './solution';

describe('Fetch Urls With Callbacks | Interview | Testcases', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

const createFetchUrlMock = (
delays: Record<string, number>,
): Mock<(url: string) => Promise<unknown>> => {
return vi.fn((url: string) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`result-${url}`);
}, delays[url] || 0);
});
});
};

it('#1 Should call callback with results in original order after all requests complete', async () => {
const urls = ['A', 'B', 'C'];
const delays = { A: 150, B: 30, C: 80 };
const fetchUrl = createFetchUrlMock(delays);
const callback = vi.fn();

fetchUrlsWithCallback(urls, fetchUrl, callback);

expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(30);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(50);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(70);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(['result-A', 'result-B', 'result-C']);
});

it('#2 Should handle empty urls array', () => {
const fetchUrl = vi.fn();
const callback = vi.fn();

fetchUrlsWithCallback([], fetchUrl, callback);

expect(fetchUrl).not.toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith([]);
});

it('#3 Should handle a single url', async () => {
const urls = ['A'];
const delays = { A: 50 };
const fetchUrl = createFetchUrlMock(delays);
const callback = vi.fn();

fetchUrlsWithCallback(urls, fetchUrl, callback);

await vi.advanceTimersByTimeAsync(50);
expect(callback).toHaveBeenCalledWith(['result-A']);
});

it('#4 Should call callback exactly once even if some promises resolve early', async () => {
const urls = ['A', 'B', 'C'];
const delays = { A: 10, B: 100, C: 200 };
const fetchUrl = createFetchUrlMock(delays);
const callback = vi.fn();

fetchUrlsWithCallback(urls, fetchUrl, callback);

await vi.advanceTimersByTimeAsync(10);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(90);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(100);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(['result-A', 'result-B', 'result-C']);
});

it('#5 Should preserve order even if responses arrive in different order', async () => {
const urls = ['X', 'Y', 'Z'];

const delays = { X: 100, Y: 20, Z: 150 };
const fetchUrl = createFetchUrlMock(delays);
const callback = vi.fn();

fetchUrlsWithCallback(urls, fetchUrl, callback);

await vi.advanceTimersByTimeAsync(20);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(80);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(50);
expect(callback).toHaveBeenCalledWith(['result-X', 'result-Y', 'result-Z']);
});

it('#6 Should work with zero delay', async () => {
const urls = ['A', 'B'];
const fetchUrl = createFetchUrlMock({ A: 0, B: 0 });
const callback = vi.fn();

fetchUrlsWithCallback(urls, fetchUrl, callback);

await vi.advanceTimersByTimeAsync(0);
expect(callback).toHaveBeenCalledWith(['result-A', 'result-B']);
});

it('#7 Should not call callback before all promises settle', async () => {
const urls = ['A', 'B', 'C'];
const delays = { A: 100, B: 100, C: 100 };
const fetchUrl = createFetchUrlMock(delays);
const callback = vi.fn();

fetchUrlsWithCallback(urls, fetchUrl, callback);

await vi.advanceTimersByTimeAsync(99);
expect(callback).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(1);
expect(callback).toHaveBeenCalledTimes(1);
});
});
52 changes: 52 additions & 0 deletions src/interview/fetch-urls-with-callback/solution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
type AsyncFn = (url: string) => Promise<unknown>;
type Callback = (results: unknown[]) => unknown;

/**
* Runs multiple asynchronous requests in parallel
* and calls the callback after all requests are completed.
*
* Results are passed to the callback in the same order
* as the corresponding URLs appear in the urls array.
*
* @param {string[]} urls - An array of URLs to fetch.
* @param {(url: string) => Promise<unknown>} fetchUrl - An asynchronous function that fetches a single URL.
* @param {(results: unknown[]) => void} callback - A function called after all requests are completed.
* @returns {void}
*/
export const fetchUrlsWithCallback = (
urls: string[],
fetchUrl: AsyncFn,
callback: Callback,
): void => {
if (urls.length === 0) {
callback([]);
return;
}

const results = Array.from({ length: urls.length });
let fetched = 0;

for (let i = 0; i < urls.length; i++) {
fetchUrl(urls[i])
.then((res) => {
results[i] = res;
fetched++;
if (fetched === urls.length) {
callback(results);
}
})
.catch(() => {
/* empty */
});
}
};

export function fetchUrl(url: string): Promise<unknown> {
const delay = { A: 150, B: 30, C: 80 }[url];

return new Promise((resolve) => {
setTimeout(() => {
resolve(`result-${url}`);
}, delay);
});
}
Loading