From 031c0c930d843a6e28b80c6d50a87a08e94313a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Louren=C3=A7o?= Date: Mon, 11 Dec 2023 12:50:05 +0000 Subject: [PATCH] feat: create map function --- src/entrypoints/map.ts | 55 ++++++++++++++++++++++++++++ src/tests/map.test.ts | 81 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/entrypoints/map.ts create mode 100644 src/tests/map.test.ts diff --git a/src/entrypoints/map.ts b/src/entrypoints/map.ts new file mode 100644 index 0000000..74724ea --- /dev/null +++ b/src/entrypoints/map.ts @@ -0,0 +1,55 @@ +interface MapFulfilledResult { + status: "fulfilled"; + value: T; +} + +interface MapRejectedResult { + status: "rejected"; + reason: any; +} + +interface MapProcessing { + status: "processing"; +} + +type MapResult = MapFulfilledResult | MapRejectedResult; + +export const map = async ( + data: T[], + callbackfn: (value: T, index: number, array: T[]) => Promise, + options?: { concurrency?: number } +): Promise[]> => { + const { concurrency = 2 } = options ?? {}; + + const results: MapResult[] = []; + + await Promise.allSettled( + Array.from({ length: concurrency }).map((_, index) => + worker(data, callbackfn, results) + ) + ); + + return results; +}; + +const worker = async ( + data: T[], + callbackfn: (value: T, index: number, array: T[]) => Promise, + results: Array | MapProcessing> +) => { + for (let index = 0; index < data.length; index++) { + const item = data[index]; + const result = results[index]; + if (!result) { + results[index] = { status: "processing" }; + try { + results[index] = { + status: "fulfilled", + value: await callbackfn(item, index, data), + }; + } catch (error) { + results[index] = { status: "rejected", reason: error }; + } + } + } +}; diff --git a/src/tests/map.test.ts b/src/tests/map.test.ts new file mode 100644 index 0000000..87c4d31 --- /dev/null +++ b/src/tests/map.test.ts @@ -0,0 +1,81 @@ +import { expect, describe, it, vi, afterEach, beforeEach } from "vitest"; +import { sleep } from "../entrypoints/sleep"; +import { map } from "../entrypoints/map"; +import exp from "constants"; +import { error } from "console"; + +describe("Map", () => { + const fn = vi.fn(async (value: number) => { + await sleep(1000); + return value * 3; + }); + + const data = [1, 2, 3, 4, 5]; + + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should map function with two workers without giving any parameters ", async () => { + const expected = [3, 6, 9, 12, 15].map((value) => ({ + status: "fulfilled", + value, + })); + + const result = map(data, fn); + expect(vi.getTimerCount()).toBe(2); + + await vi.runAllTimersAsync(); + expect(await result).toEqual(expected); + expect(fn).toHaveBeenCalledTimes(5); + expect(fn).toHaveBeenCalledWith(1, 0, data); + expect(fn).toHaveBeenCalledWith(2, 1, data); + expect(fn).toHaveBeenCalledWith(3, 2, data); + expect(fn).toHaveBeenCalledWith(4, 3, data); + expect(fn).toHaveBeenCalledWith(5, 4, data); + }); + + it("should map function with five workers parameter", async () => { + const expected = [3, 6, 9, 12, 15].map((value) => ({ + status: "fulfilled", + value, + })); + + const result = map(data, fn, { concurrency: 5 }); + expect(vi.getTimerCount()).toBe(5); + + await vi.runAllTimersAsync(); + expect(await result).toEqual(expected); + expect(fn).toHaveBeenCalledTimes(5); + expect(fn).toHaveBeenCalledWith(1, 0, data); + expect(fn).toHaveBeenCalledWith(2, 1, data); + expect(fn).toHaveBeenCalledWith(3, 2, data); + expect(fn).toHaveBeenCalledWith(4, 3, data); + expect(fn).toHaveBeenCalledWith(5, 4, data); + }); + + it("should return fail results", async () => { + const fn = vi.fn(async (value: number) => { + throw new Error(`fail to ${value}`); + }); + + const expected = data.map((value) => ({ + status: "rejected", + reason: new Error(`fail to ${value}`), + })); + + const result = await map(data, fn); + + expect(result).toEqual(expected); + expect(fn).toHaveBeenCalledTimes(5); + expect(fn).toHaveBeenCalledWith(1, 0, data); + expect(fn).toHaveBeenCalledWith(2, 1, data); + expect(fn).toHaveBeenCalledWith(3, 2, data); + expect(fn).toHaveBeenCalledWith(4, 3, data); + expect(fn).toHaveBeenCalledWith(5, 4, data); + }); +});