/ tests / unit / mutexExecutor.test.ts
mutexExecutor.test.ts
  1  import { describe, expect, test, vi } from "vitest";
  2  
  3  import * as mutexExecutor from "@app/lib/mutexExecutor";
  4  import { sleep } from "@app/lib/sleep";
  5  
  6  describe("executor", () => {
  7    test("cancels running task", async () => {
  8      const e = mutexExecutor.create();
  9  
 10      const first = e.run(async () => {
 11        await sleep(10);
 12        return "first";
 13      });
 14      const second = e.run(async () => {
 15        return "second";
 16      });
 17  
 18      expect(await first).toBe(undefined);
 19      expect(await second).toBe("second");
 20  
 21      const third = e.run(async () => {
 22        await sleep(10);
 23        return "third";
 24      });
 25      const fourth = e.run(async () => {
 26        return "fourth";
 27      });
 28  
 29      expect(await third).toBe(undefined);
 30      expect(await fourth).toBe("fourth");
 31    });
 32  
 33    test("cancels multiple tasks", async () => {
 34      const e = mutexExecutor.create();
 35  
 36      const canceled1 = e.run(async () => {
 37        await sleep(10);
 38        return true;
 39      });
 40      const canceled2 = e.run(async () => {
 41        await sleep(10);
 42        return true;
 43      });
 44      const canceled3 = e.run(async () => {
 45        await sleep(10);
 46        return true;
 47      });
 48      const last = e.run(async () => {
 49        return true;
 50      });
 51  
 52      expect(await canceled1).toBe(undefined);
 53      expect(await canceled2).toBe(undefined);
 54      expect(await canceled3).toBe(undefined);
 55      expect(await last).toBe(true);
 56    });
 57  
 58    test("triggers abort signal event", async () => {
 59      const e = mutexExecutor.create();
 60      const abortListener = vi.fn();
 61  
 62      void e.run(async abort => {
 63        abort.addEventListener("abort", abortListener);
 64        await sleep(10);
 65        return "first";
 66      });
 67      expect(abortListener).not.toHaveBeenCalled();
 68      // eslint-disable-next-line @typescript-eslint/no-empty-function
 69      void e.run(async () => {});
 70      expect(abortListener).toHaveBeenCalled();
 71    });
 72  
 73    test("don’t throw error on aborted task", async () => {
 74      const e = mutexExecutor.create();
 75  
 76      const first = e.run(async () => {
 77        await sleep(10);
 78        throw new Error();
 79      });
 80      const second = e.run(async () => {
 81        return "second";
 82      });
 83  
 84      expect(await first).toBe(undefined);
 85      expect(await second).toBe("second");
 86    });
 87  });
 88  
 89  describe("worker", () => {
 90    test("sequential work", async () => {
 91      const w = mutexExecutor.createWorker(async (value: number) => {
 92        await sleep(10);
 93        return value;
 94      });
 95  
 96      const outputs: number[] = [];
 97      w.output.onValue(value => outputs.push(value));
 98  
 99      await w.submit(1);
100      await w.submit(2);
101      await w.submit(3);
102  
103      expect(outputs).toEqual([1, 2, 3]);
104    });
105  
106    test("overlapping work cancels", async () => {
107      const w = mutexExecutor.createWorker(async (value: number) => {
108        await sleep(10);
109        return value;
110      });
111  
112      const nextOutput = w.output.firstToPromise();
113  
114      void w.submit(1);
115      void w.submit(2);
116      void w.submit(3);
117  
118      expect(await nextOutput).toEqual(3);
119    });
120  });