Skip to content
Draft
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
55 changes: 55 additions & 0 deletions src/entrypoints/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
interface MapFulfilledResult<T> {
status: "fulfilled";
value: T;
}

interface MapRejectedResult {
status: "rejected";
reason: any;
}

interface MapProcessing {
status: "processing";
}

type MapResult<T> = MapFulfilledResult<T> | MapRejectedResult;

export const map = async <T, R>(
data: T[],
callbackfn: (value: T, index: number, array: T[]) => Promise<R>,
options?: { concurrency?: number }
): Promise<MapResult<R>[]> => {
const { concurrency = 2 } = options ?? {};

const results: MapResult<R>[] = [];

await Promise.allSettled(
Array.from({ length: concurrency }).map((_, index) =>
worker(data, callbackfn, results)
)
);

return results;
};

const worker = async <T, R>(
data: T[],
callbackfn: (value: T, index: number, array: T[]) => Promise<R>,
results: Array<MapResult<R> | 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 };
}
}
}
};
81 changes: 81 additions & 0 deletions src/tests/map.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});