From 795af3af23dcb817a82829c2555249ae5aabdf55 Mon Sep 17 00:00:00 2001 From: TheGeniusOfEternity Date: Sat, 29 Aug 2026 21:26:33 +0300 Subject: [PATCH 1/3] test(interview|fetch-urls-with-callback): testcases --- .../fetch-urls-with-callback/solution.test.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/interview/fetch-urls-with-callback/solution.test.ts diff --git a/src/interview/fetch-urls-with-callback/solution.test.ts b/src/interview/fetch-urls-with-callback/solution.test.ts new file mode 100644 index 0000000..8e74370 --- /dev/null +++ b/src/interview/fetch-urls-with-callback/solution.test.ts @@ -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, + ): Mock<(url: string) => Promise> => { + 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); + }); +}); From ddd6f14f211c0f6a3945b61c3b71bf9e328e0a4f Mon Sep 17 00:00:00 2001 From: TheGeniusOfEternity Date: Sat, 29 Aug 2026 21:26:45 +0300 Subject: [PATCH 2/3] feat(interview|fetch-urls-with-callback): solution --- .../fetch-urls-with-callback/solution.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/interview/fetch-urls-with-callback/solution.ts diff --git a/src/interview/fetch-urls-with-callback/solution.ts b/src/interview/fetch-urls-with-callback/solution.ts new file mode 100644 index 0000000..0932aa5 --- /dev/null +++ b/src/interview/fetch-urls-with-callback/solution.ts @@ -0,0 +1,52 @@ +type AsyncFn = (url: string) => Promise; +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} 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 { + const delay = { A: 150, B: 30, C: 80 }[url]; + + return new Promise((resolve) => { + setTimeout(() => { + resolve(`result-${url}`); + }, delay); + }); +} From f60922c1188d3025fd5a5f3a9108852d995b386a Mon Sep 17 00:00:00 2001 From: TheGeniusOfEternity Date: Sat, 29 Aug 2026 21:26:56 +0300 Subject: [PATCH 3/3] feat(interview): update table of contents --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 33e0498..6367be2 100644 --- a/README.md +++ b/README.md @@ -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_ \ No newline at end of file +- [Task #16](src/interview/digit-permutation) - _Digit Permutation_ +- [Task #17](src/interview/fetch-urls-with-callback) - _Fetch Urls With Callback_ \ No newline at end of file