@praxisjs/concurrent 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @praxisjs/concurrent
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [fe39901]
8
+ - @praxisjs/core@0.4.1
9
+
10
+ ## 0.2.1
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [f52354d]
15
+ - @praxisjs/core@0.4.0
16
+
3
17
  ## 0.2.0
4
18
 
5
19
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=decorators.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Task, Queue, Pool } from "../decorators";
3
+ function methodCtx(name) {
4
+ const initializers = [];
5
+ return {
6
+ ctx: {
7
+ name,
8
+ kind: "method",
9
+ addInitializer(fn) { initializers.push(fn); },
10
+ },
11
+ run(instance) { initializers.forEach((fn) => { fn.call(instance); }); },
12
+ };
13
+ }
14
+ // ── Task ──────────────────────────────────────────────────────────────────────
15
+ describe("Task decorator", () => {
16
+ it("replaces method with a task runner and exposes signals", async () => {
17
+ const { ctx, run } = methodCtx("load");
18
+ const original = async (x) => x * 2;
19
+ Task()(original, ctx);
20
+ const instance = {};
21
+ run(instance);
22
+ expect(typeof instance.load).toBe("function");
23
+ expect(instance.load_loading).toBeDefined();
24
+ expect(instance.load_error).toBeDefined();
25
+ expect(instance.load_lastResult).toBeDefined();
26
+ const result = await instance.load(5);
27
+ expect(result).toBe(10);
28
+ expect(instance.load_lastResult()).toBe(10);
29
+ });
30
+ it("sets loading signal while running", async () => {
31
+ const { ctx, run } = methodCtx("fetch");
32
+ let resolve;
33
+ const original = async () => new Promise((r) => { resolve = r; });
34
+ Task()(original, ctx);
35
+ const instance = {};
36
+ run(instance);
37
+ const p = instance.fetch();
38
+ expect(instance.fetch_loading()).toBe(true);
39
+ resolve("done");
40
+ await p;
41
+ expect(instance.fetch_loading()).toBe(false);
42
+ });
43
+ });
44
+ // ── Queue ─────────────────────────────────────────────────────────────────────
45
+ describe("Queue decorator", () => {
46
+ it("replaces method with a queue runner and exposes signals", async () => {
47
+ const { ctx, run } = methodCtx("save");
48
+ const original = async (x) => String(x);
49
+ Queue()(original, ctx);
50
+ const instance = {};
51
+ run(instance);
52
+ expect(typeof instance.save).toBe("function");
53
+ expect(instance.save_loading).toBeDefined();
54
+ expect(instance.save_pending).toBeDefined();
55
+ expect(instance.save_error).toBeDefined();
56
+ const result = await instance.save("hello");
57
+ expect(result).toBe("hello");
58
+ });
59
+ });
60
+ // ── Pool ──────────────────────────────────────────────────────────────────────
61
+ describe("Pool decorator", () => {
62
+ it("replaces method with a pool runner and exposes signals", async () => {
63
+ const { ctx, run } = methodCtx("process");
64
+ const original = async (x) => x + 1;
65
+ Pool(2)(original, ctx);
66
+ const instance = {};
67
+ run(instance);
68
+ expect(typeof instance.process).toBe("function");
69
+ expect(instance.process_loading).toBeDefined();
70
+ expect(instance.process_active).toBeDefined();
71
+ expect(instance.process_pending).toBeDefined();
72
+ expect(instance.process_error).toBeDefined();
73
+ const result = await instance.process(9);
74
+ expect(result).toBe(10);
75
+ });
76
+ it("respects concurrency limit", async () => {
77
+ const { ctx, run } = methodCtx("work");
78
+ const resolvers = [];
79
+ const original = async () => new Promise((r) => resolvers.push(r));
80
+ Pool(2)(original, ctx);
81
+ const instance = {};
82
+ run(instance);
83
+ const work = instance.work;
84
+ const t1 = work();
85
+ const t2 = work();
86
+ work(); // queued — concurrency limit is 2
87
+ expect(instance.work_active()).toBe(2);
88
+ expect(instance.work_pending()).toBe(1);
89
+ resolvers.forEach((r) => { r(); });
90
+ await Promise.all([t1, t2]);
91
+ });
92
+ });
93
+ //# sourceMappingURL=decorators.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.test.js","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/pool.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { pool } from "../pool";
3
+ describe("pool", () => {
4
+ it("executes a task and resolves with its result", async () => {
5
+ const p = pool(2, async (x) => x * 3);
6
+ expect(await p(4)).toBe(12);
7
+ });
8
+ it("respects concurrency limit", async () => {
9
+ let active = 0;
10
+ let maxActive = 0;
11
+ const p = pool(2, async () => {
12
+ active++;
13
+ maxActive = Math.max(maxActive, active);
14
+ await new Promise((r) => setTimeout(r, 10));
15
+ active--;
16
+ });
17
+ await Promise.all([p(), p(), p(), p()]);
18
+ expect(maxActive).toBeLessThanOrEqual(2);
19
+ });
20
+ it("active() reflects currently running tasks", async () => {
21
+ const resolvers = [];
22
+ const p = pool(3, () => new Promise((r) => resolvers.push(r)));
23
+ const tasks = [p(), p()];
24
+ expect(p.active()).toBe(2);
25
+ resolvers.forEach((r) => { r(); });
26
+ await Promise.all(tasks);
27
+ expect(p.active()).toBe(0);
28
+ });
29
+ it("loading is true when active > 0", async () => {
30
+ let resolve;
31
+ const p = pool(1, () => new Promise((r) => { resolve = r; }));
32
+ const task = p();
33
+ expect(p.loading()).toBe(true);
34
+ resolve();
35
+ await task;
36
+ expect(p.loading()).toBe(false);
37
+ });
38
+ it("captures errors in the error signal and resolves undefined", async () => {
39
+ const p = pool(1, async () => {
40
+ throw new Error("pool error");
41
+ });
42
+ const result = await p();
43
+ expect(result).toBeUndefined();
44
+ expect(p.error()?.message).toBe("pool error");
45
+ });
46
+ it("pending count decrements as tasks start", async () => {
47
+ let resolveFirst;
48
+ const started = [];
49
+ const p = pool(1, async (idx) => {
50
+ started.push(idx);
51
+ if (idx === 0)
52
+ await new Promise((r) => { resolveFirst = r; });
53
+ });
54
+ const t1 = p(0);
55
+ const t2 = p(1);
56
+ // second task is pending while first runs
57
+ expect(p.pending()).toBe(1);
58
+ resolveFirst();
59
+ await t1;
60
+ await t2;
61
+ expect(p.pending()).toBe(0);
62
+ });
63
+ });
64
+ //# sourceMappingURL=pool.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.test.js","sourceRoot":"","sources":["../../src/__tests__/pool.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=queue.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/queue.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { queue } from "../queue";
3
+ describe("queue", () => {
4
+ it("executes the task and resolves the promise", async () => {
5
+ const q = queue(async (x) => x * 2);
6
+ const result = await q(5);
7
+ expect(result).toBe(10);
8
+ });
9
+ it("runs tasks sequentially (FIFO)", async () => {
10
+ const order = [];
11
+ const q = queue(async (n) => {
12
+ order.push(n);
13
+ return n;
14
+ });
15
+ await Promise.all([q(1), q(2), q(3)]);
16
+ expect(order).toEqual([1, 2, 3]);
17
+ });
18
+ it("loading is true while running, false when done", async () => {
19
+ let resolve;
20
+ const q = queue(() => new Promise((r) => { resolve = r; }));
21
+ const p = q();
22
+ expect(q.loading()).toBe(true);
23
+ resolve();
24
+ await p;
25
+ expect(q.loading()).toBe(false);
26
+ });
27
+ it("pending decrements as tasks complete", async () => {
28
+ let resolveFirst;
29
+ const q = queue((_, idx) => idx === 0
30
+ ? new Promise((r) => { resolveFirst = r; })
31
+ : Promise.resolve());
32
+ const p1 = q(null, 0);
33
+ const p2 = q(null, 1);
34
+ // Both enqueued — first is running, second is pending
35
+ expect(q.pending()).toBeGreaterThanOrEqual(0);
36
+ resolveFirst();
37
+ await p1;
38
+ await p2;
39
+ expect(q.pending()).toBe(0);
40
+ });
41
+ it("captures errors in the error signal", async () => {
42
+ const q = queue(async () => {
43
+ throw new Error("boom");
44
+ });
45
+ await expect(q()).rejects.toThrow("boom");
46
+ expect(q.error()?.message).toBe("boom");
47
+ });
48
+ it("clear() empties the pending queue", async () => {
49
+ let resolveFirst;
50
+ const q = queue((_, idx) => idx === 0
51
+ ? new Promise((r) => { resolveFirst = r; })
52
+ : Promise.resolve());
53
+ q(null, 0);
54
+ q(null, 1);
55
+ q(null, 2);
56
+ q.clear();
57
+ resolveFirst();
58
+ // After clearing, pending count should be 0
59
+ expect(q.pending()).toBe(0);
60
+ });
61
+ });
62
+ //# sourceMappingURL=queue.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.test.js","sourceRoot":"","sources":["../../src/__tests__/queue.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=task.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/task.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { task } from "../task";
3
+ describe("task", () => {
4
+ it("resolves with the function result", async () => {
5
+ const t = task(async (x) => x + 1);
6
+ expect(await t(9)).toBe(10);
7
+ });
8
+ it("sets loading to true while running", async () => {
9
+ let resolve;
10
+ const t = task(() => new Promise((r) => { resolve = r; }));
11
+ const p = t();
12
+ expect(t.loading()).toBe(true);
13
+ resolve(42);
14
+ await p;
15
+ expect(t.loading()).toBe(false);
16
+ });
17
+ it("stores last result", async () => {
18
+ const t = task(async () => "hello");
19
+ await t();
20
+ expect(t.lastResult()).toBe("hello");
21
+ });
22
+ it("captures errors in the error signal", async () => {
23
+ const t = task(async () => {
24
+ throw new Error("task failed");
25
+ });
26
+ const result = await t();
27
+ expect(result).toBeUndefined();
28
+ expect(t.error()?.message).toBe("task failed");
29
+ });
30
+ it("clears error on a new run", async () => {
31
+ let shouldFail = true;
32
+ const t = task(async () => {
33
+ if (shouldFail)
34
+ throw new Error("oops");
35
+ return "ok";
36
+ });
37
+ await t();
38
+ expect(t.error()).not.toBeNull();
39
+ shouldFail = false;
40
+ await t();
41
+ expect(t.error()).toBeNull();
42
+ expect(t.lastResult()).toBe("ok");
43
+ });
44
+ it("cancelAll() discards in-flight results", async () => {
45
+ let resolve;
46
+ const t = task(() => new Promise((r) => { resolve = r; }));
47
+ const p = t();
48
+ t.cancelAll();
49
+ resolve("stale");
50
+ const result = await p;
51
+ expect(result).toBeUndefined();
52
+ expect(t.loading()).toBe(false);
53
+ });
54
+ });
55
+ //# sourceMappingURL=task.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.test.js","sourceRoot":"","sources":["../../src/__tests__/task.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/concurrent",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,7 +14,7 @@
14
14
  "typescript": "^5.9.3"
15
15
  },
16
16
  "dependencies": {
17
- "@praxisjs/core": "0.3.0",
17
+ "@praxisjs/core": "0.4.1",
18
18
  "@praxisjs/shared": "0.2.0"
19
19
  },
20
20
  "scripts": {
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { Task, Queue, Pool } from "../decorators";
4
+
5
+ function methodCtx(name: string) {
6
+ const initializers: Array<(this: unknown) => void> = [];
7
+ return {
8
+ ctx: {
9
+ name,
10
+ kind: "method" as const,
11
+ addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
12
+ } as ClassMethodDecoratorContext,
13
+ run(instance: unknown) { initializers.forEach((fn) => { fn.call(instance); }); },
14
+ };
15
+ }
16
+
17
+ // ── Task ──────────────────────────────────────────────────────────────────────
18
+
19
+ describe("Task decorator", () => {
20
+ it("replaces method with a task runner and exposes signals", async () => {
21
+ const { ctx, run } = methodCtx("load");
22
+ const original = async (x: unknown) => (x as number) * 2;
23
+ Task()(original, ctx);
24
+
25
+ const instance: Record<string, unknown> = {};
26
+ run(instance);
27
+
28
+ expect(typeof instance.load).toBe("function");
29
+ expect(instance.load_loading).toBeDefined();
30
+ expect(instance.load_error).toBeDefined();
31
+ expect(instance.load_lastResult).toBeDefined();
32
+
33
+ const result = await (instance.load as (x: number) => Promise<number>)(5);
34
+ expect(result).toBe(10);
35
+ expect(
36
+ (instance.load_lastResult as () => number)(),
37
+ ).toBe(10);
38
+ });
39
+
40
+ it("sets loading signal while running", async () => {
41
+ const { ctx, run } = methodCtx("fetch");
42
+ let resolve!: (v: string) => void;
43
+ const original = async () => new Promise<string>((r) => { resolve = r; });
44
+ Task()(original, ctx);
45
+
46
+ const instance: Record<string, unknown> = {};
47
+ run(instance);
48
+
49
+ const p = (instance.fetch as () => Promise<string>)();
50
+ expect((instance.fetch_loading as () => boolean)()).toBe(true);
51
+ resolve("done");
52
+ await p;
53
+ expect((instance.fetch_loading as () => boolean)()).toBe(false);
54
+ });
55
+ });
56
+
57
+ // ── Queue ─────────────────────────────────────────────────────────────────────
58
+
59
+ describe("Queue decorator", () => {
60
+ it("replaces method with a queue runner and exposes signals", async () => {
61
+ const { ctx, run } = methodCtx("save");
62
+ const original = async (x: unknown) => String(x);
63
+ Queue()(original, ctx);
64
+
65
+ const instance: Record<string, unknown> = {};
66
+ run(instance);
67
+
68
+ expect(typeof instance.save).toBe("function");
69
+ expect(instance.save_loading).toBeDefined();
70
+ expect(instance.save_pending).toBeDefined();
71
+ expect(instance.save_error).toBeDefined();
72
+
73
+ const result = await (instance.save as (x: string) => Promise<string>)("hello");
74
+ expect(result).toBe("hello");
75
+ });
76
+ });
77
+
78
+ // ── Pool ──────────────────────────────────────────────────────────────────────
79
+
80
+ describe("Pool decorator", () => {
81
+ it("replaces method with a pool runner and exposes signals", async () => {
82
+ const { ctx, run } = methodCtx("process");
83
+ const original = async (x: unknown) => (x as number) + 1;
84
+ Pool(2)(original, ctx);
85
+
86
+ const instance: Record<string, unknown> = {};
87
+ run(instance);
88
+
89
+ expect(typeof instance.process).toBe("function");
90
+ expect(instance.process_loading).toBeDefined();
91
+ expect(instance.process_active).toBeDefined();
92
+ expect(instance.process_pending).toBeDefined();
93
+ expect(instance.process_error).toBeDefined();
94
+
95
+ const result = await (instance.process as (x: number) => Promise<number>)(9);
96
+ expect(result).toBe(10);
97
+ });
98
+
99
+ it("respects concurrency limit", async () => {
100
+ const { ctx, run } = methodCtx("work");
101
+ const resolvers: Array<() => void> = [];
102
+ const original = async () => new Promise<void>((r) => resolvers.push(r));
103
+ Pool(2)(original, ctx);
104
+
105
+ const instance: Record<string, unknown> = {};
106
+ run(instance);
107
+
108
+ const work = instance.work as () => Promise<void>;
109
+ const t1 = work();
110
+ const t2 = work();
111
+ work(); // queued — concurrency limit is 2
112
+
113
+ expect((instance.work_active as () => number)()).toBe(2);
114
+ expect((instance.work_pending as () => number)()).toBe(1);
115
+
116
+ resolvers.forEach((r) => { r(); });
117
+ await Promise.all([t1, t2]);
118
+ });
119
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { pool } from "../pool";
4
+
5
+ describe("pool", () => {
6
+ it("executes a task and resolves with its result", async () => {
7
+ const p = pool(2, async (x: unknown) => (x as number) * 3);
8
+ expect(await p(4)).toBe(12);
9
+ });
10
+
11
+ it("respects concurrency limit", async () => {
12
+ let active = 0;
13
+ let maxActive = 0;
14
+
15
+ const p = pool(2, async () => {
16
+ active++;
17
+ maxActive = Math.max(maxActive, active);
18
+ await new Promise((r) => setTimeout(r, 10));
19
+ active--;
20
+ });
21
+
22
+ await Promise.all([p(), p(), p(), p()]);
23
+ expect(maxActive).toBeLessThanOrEqual(2);
24
+ });
25
+
26
+ it("active() reflects currently running tasks", async () => {
27
+ const resolvers: Array<() => void> = [];
28
+ const p = pool(
29
+ 3,
30
+ () => new Promise<void>((r) => resolvers.push(r)),
31
+ );
32
+
33
+ const tasks = [p(), p()];
34
+ expect(p.active()).toBe(2);
35
+ resolvers.forEach((r) => { r(); });
36
+ await Promise.all(tasks);
37
+ expect(p.active()).toBe(0);
38
+ });
39
+
40
+ it("loading is true when active > 0", async () => {
41
+ let resolve!: () => void;
42
+ const p = pool(
43
+ 1,
44
+ () => new Promise<void>((r) => { resolve = r; }),
45
+ );
46
+ const task = p();
47
+ expect(p.loading()).toBe(true);
48
+ resolve();
49
+ await task;
50
+ expect(p.loading()).toBe(false);
51
+ });
52
+
53
+ it("captures errors in the error signal and resolves undefined", async () => {
54
+ const p = pool(1, async () => {
55
+ throw new Error("pool error");
56
+ });
57
+ const result = await p();
58
+ expect(result).toBeUndefined();
59
+ expect(p.error()?.message).toBe("pool error");
60
+ });
61
+
62
+ it("pending count decrements as tasks start", async () => {
63
+ let resolveFirst!: () => void;
64
+ const started: number[] = [];
65
+ const p = pool(1, async (idx: unknown) => {
66
+ started.push(idx as number);
67
+ if (idx === 0) await new Promise<void>((r) => { resolveFirst = r; });
68
+ });
69
+
70
+ const t1 = p(0);
71
+ const t2 = p(1);
72
+
73
+ // second task is pending while first runs
74
+ expect(p.pending()).toBe(1);
75
+ resolveFirst();
76
+ await t1;
77
+ await t2;
78
+ expect(p.pending()).toBe(0);
79
+ });
80
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { queue } from "../queue";
4
+
5
+ describe("queue", () => {
6
+ it("executes the task and resolves the promise", async () => {
7
+ const q = queue(async (x: unknown) => (x as number) * 2);
8
+ const result = await q(5);
9
+ expect(result).toBe(10);
10
+ });
11
+
12
+ it("runs tasks sequentially (FIFO)", async () => {
13
+ const order: number[] = [];
14
+ const q = queue(async (n: unknown) => {
15
+ order.push(n as number);
16
+ return n as number;
17
+ });
18
+ await Promise.all([q(1), q(2), q(3)]);
19
+ expect(order).toEqual([1, 2, 3]);
20
+ });
21
+
22
+ it("loading is true while running, false when done", async () => {
23
+ let resolve!: () => void;
24
+ const q = queue(
25
+ () => new Promise<void>((r) => { resolve = r; }),
26
+ );
27
+ const p = q();
28
+ expect(q.loading()).toBe(true);
29
+ resolve();
30
+ await p;
31
+ expect(q.loading()).toBe(false);
32
+ });
33
+
34
+ it("pending decrements as tasks complete", async () => {
35
+ let resolveFirst!: () => void;
36
+ const q = queue(
37
+ (_: unknown, idx: unknown) =>
38
+ idx === 0
39
+ ? new Promise<void>((r) => { resolveFirst = r; })
40
+ : Promise.resolve(),
41
+ );
42
+
43
+ const p1 = q(null, 0);
44
+ const p2 = q(null, 1);
45
+
46
+ // Both enqueued — first is running, second is pending
47
+ expect(q.pending()).toBeGreaterThanOrEqual(0);
48
+
49
+ resolveFirst();
50
+ await p1;
51
+ await p2;
52
+
53
+ expect(q.pending()).toBe(0);
54
+ });
55
+
56
+ it("captures errors in the error signal", async () => {
57
+ const q = queue(async () => {
58
+ throw new Error("boom");
59
+ });
60
+ await expect(q()).rejects.toThrow("boom");
61
+ expect(q.error()?.message).toBe("boom");
62
+ });
63
+
64
+ it("clear() empties the pending queue", async () => {
65
+ let resolveFirst!: () => void;
66
+ const q = queue(
67
+ (_: unknown, idx: unknown) =>
68
+ idx === 0
69
+ ? new Promise<void>((r) => { resolveFirst = r; })
70
+ : Promise.resolve(),
71
+ );
72
+
73
+ q(null, 0);
74
+ q(null, 1);
75
+ q(null, 2);
76
+
77
+ q.clear();
78
+ resolveFirst();
79
+
80
+ // After clearing, pending count should be 0
81
+ expect(q.pending()).toBe(0);
82
+ });
83
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { task } from "../task";
4
+
5
+ describe("task", () => {
6
+ it("resolves with the function result", async () => {
7
+ const t = task(async (x: unknown) => (x as number) + 1);
8
+ expect(await t(9)).toBe(10);
9
+ });
10
+
11
+ it("sets loading to true while running", async () => {
12
+ let resolve!: (v: number) => void;
13
+ const t = task(() => new Promise<number>((r) => { resolve = r; }));
14
+
15
+ const p = t();
16
+ expect(t.loading()).toBe(true);
17
+ resolve(42);
18
+ await p;
19
+ expect(t.loading()).toBe(false);
20
+ });
21
+
22
+ it("stores last result", async () => {
23
+ const t = task(async () => "hello");
24
+ await t();
25
+ expect(t.lastResult()).toBe("hello");
26
+ });
27
+
28
+ it("captures errors in the error signal", async () => {
29
+ const t = task(async () => {
30
+ throw new Error("task failed");
31
+ });
32
+ const result = await t();
33
+ expect(result).toBeUndefined();
34
+ expect(t.error()?.message).toBe("task failed");
35
+ });
36
+
37
+ it("clears error on a new run", async () => {
38
+ let shouldFail = true;
39
+ const t = task(async () => {
40
+ if (shouldFail) throw new Error("oops");
41
+ return "ok";
42
+ });
43
+ await t();
44
+ expect(t.error()).not.toBeNull();
45
+
46
+ shouldFail = false;
47
+ await t();
48
+ expect(t.error()).toBeNull();
49
+ expect(t.lastResult()).toBe("ok");
50
+ });
51
+
52
+ it("cancelAll() discards in-flight results", async () => {
53
+ let resolve!: (v: string) => void;
54
+ const t = task(() => new Promise<string>((r) => { resolve = r; }));
55
+
56
+ const p = t();
57
+ t.cancelAll();
58
+ resolve("stale");
59
+ const result = await p;
60
+
61
+ expect(result).toBeUndefined();
62
+ expect(t.loading()).toBe(false);
63
+ });
64
+ });