@cplieger/actions 1.0.1 → 1.0.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.
@@ -1,121 +0,0 @@
1
- // @vitest-environment happy-dom
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
3
- vi.mock("./notifier.js", () => ({
4
- configure: vi.fn(),
5
- notifySuccess: vi.fn(),
6
- notifyError: vi.fn(),
7
- _resetNotifierForTest: vi.fn(),
8
- }));
9
- import { defineAction, _resetForTest as resetDefine } from "./define.js";
10
- import { _resetForTest as resetRegistry } from "./registry.js";
11
- import { _resetForTest as resetCleanup } from "./cleanup.js";
12
-
13
- beforeEach(() => {
14
- resetDefine();
15
- resetRegistry();
16
- resetCleanup();
17
- vi.clearAllMocks();
18
- });
19
-
20
- describe("cross-action scope serialization after cancel", () => {
21
- it("C waits for A when B is cancelled (middle entry)", async () => {
22
- const order: string[] = [];
23
- let resolveA!: (v: string) => void;
24
- const actionA = defineAction<void, string>({
25
- name: "test.serial_A",
26
- scope: "serial",
27
- run: () => {
28
- order.push("A-start");
29
- return new Promise<string>((r) => {
30
- resolveA = r;
31
- });
32
- },
33
- });
34
- const actionB = defineAction<void, string>({
35
- name: "test.serial_B",
36
- scope: "serial",
37
- error: false,
38
- run: (_args, signal) => {
39
- order.push("B-start");
40
- if (signal.aborted) {
41
- throw new DOMException("aborted", "AbortError");
42
- }
43
- return Promise.resolve("B");
44
- },
45
- });
46
- const actionC = defineAction<void, string>({
47
- name: "test.serial_C",
48
- scope: "serial",
49
- run: () => {
50
- order.push("C-start");
51
- return Promise.resolve("C");
52
- },
53
- });
54
- const pA = actionA.dispatch();
55
- await Promise.resolve();
56
- expect(order).toEqual(["A-start"]);
57
- const pB = actionB.dispatch();
58
- const pC = actionC.dispatch();
59
- actionB.cancel();
60
- await Promise.resolve();
61
- await Promise.resolve();
62
- await Promise.resolve();
63
- expect(order).toEqual(["A-start"]);
64
- resolveA("A-done");
65
- const [rA, rB, rC] = await Promise.all([pA, pB, pC]);
66
- expect(rA).toBe("A-done");
67
- expect(rB).toBeNull();
68
- expect(rC).toBe("C");
69
- expect(order).toEqual(["A-start", "C-start"]);
70
- });
71
-
72
- it("D waits for A when B (last entry) is cancelled then D dispatches", async () => {
73
- const order: string[] = [];
74
- let resolveA!: (v: string) => void;
75
- const actionA = defineAction<void, string>({
76
- name: "test.last_A",
77
- scope: "last-cancel",
78
- run: () => {
79
- order.push("A-start");
80
- return new Promise<string>((r) => {
81
- resolveA = r;
82
- });
83
- },
84
- });
85
- const actionB = defineAction<void, string>({
86
- name: "test.last_B",
87
- scope: "last-cancel",
88
- error: false,
89
- run: (_args, signal) => {
90
- order.push("B-start");
91
- if (signal.aborted) {
92
- throw new DOMException("aborted", "AbortError");
93
- }
94
- return Promise.resolve("B");
95
- },
96
- });
97
- const actionD = defineAction<void, string>({
98
- name: "test.last_D",
99
- scope: "last-cancel",
100
- run: () => {
101
- order.push("D-start");
102
- return Promise.resolve("D");
103
- },
104
- });
105
- const pA = actionA.dispatch();
106
- await Promise.resolve();
107
- const pB = actionB.dispatch();
108
- actionB.cancel();
109
- const pD = actionD.dispatch();
110
- await Promise.resolve();
111
- await Promise.resolve();
112
- await Promise.resolve();
113
- expect(order).toEqual(["A-start"]);
114
- resolveA("A-done");
115
- const [rA, rB, rD] = await Promise.all([pA, pB, pD]);
116
- expect(rA).toBe("A-done");
117
- expect(rB).toBeNull();
118
- expect(rD).toBe("D");
119
- expect(order).toEqual(["A-start", "D-start"]);
120
- });
121
- });
@@ -1,121 +0,0 @@
1
- // @vitest-environment happy-dom
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
3
- vi.mock("./notifier.js", () => ({
4
- configure: vi.fn(),
5
- notifySuccess: vi.fn(),
6
- notifyError: vi.fn(),
7
- _resetNotifierForTest: vi.fn(),
8
- }));
9
- import { defineAction, _resetForTest as resetDefine } from "./define.js";
10
- import { _resetForTest as resetRegistry } from "./registry.js";
11
- import { _resetForTest as resetCleanup } from "./cleanup.js";
12
- import { debouncedDispatch } from "./debounce.js";
13
-
14
- beforeEach(() => {
15
- resetDefine();
16
- resetRegistry();
17
- resetCleanup();
18
- vi.useFakeTimers();
19
- });
20
-
21
- function makeAction() {
22
- const run = vi.fn(async (args: string) => args);
23
- const action = defineAction({ name: "test.debounce", run });
24
- return { action, run };
25
- }
26
-
27
- describe("debouncedDispatch — trailing (default)", () => {
28
- it("dispatches after the wait period", () => {
29
- const { action, run } = makeAction();
30
- const debounced = debouncedDispatch(action, { wait: 100 });
31
- debounced("a");
32
- expect(run).not.toHaveBeenCalled();
33
- vi.advanceTimersByTime(100);
34
- expect(run).toHaveBeenCalledWith("a", expect.anything(), expect.anything());
35
- });
36
-
37
- it("coalesces rapid calls — only last args dispatched", () => {
38
- const { action, run } = makeAction();
39
- const debounced = debouncedDispatch(action, { wait: 50 });
40
- debounced("a");
41
- debounced("b");
42
- debounced("c");
43
- vi.advanceTimersByTime(50);
44
- expect(run).toHaveBeenCalledTimes(1);
45
- expect(run).toHaveBeenCalledWith("c", expect.anything(), expect.anything());
46
- });
47
-
48
- it("cancel() prevents the pending dispatch", () => {
49
- const { action, run } = makeAction();
50
- const debounced = debouncedDispatch(action, { wait: 100 });
51
- debounced("a");
52
- debounced.cancel();
53
- vi.advanceTimersByTime(200);
54
- expect(run).not.toHaveBeenCalled();
55
- });
56
-
57
- it("flush() fires immediately with pending args", () => {
58
- const { action, run } = makeAction();
59
- const debounced = debouncedDispatch(action, { wait: 100 });
60
- debounced("x");
61
- debounced.flush();
62
- expect(run).toHaveBeenCalledWith("x", expect.anything(), expect.anything());
63
- });
64
-
65
- it("flush(args) overrides pending args", () => {
66
- const { action, run } = makeAction();
67
- const debounced = debouncedDispatch(action, { wait: 100 });
68
- debounced("old");
69
- debounced.flush("override");
70
- expect(run).toHaveBeenCalledWith("override", expect.anything(), expect.anything());
71
- });
72
-
73
- it("isPending() reflects scheduled state", () => {
74
- const { action } = makeAction();
75
- const debounced = debouncedDispatch(action, { wait: 100 });
76
- expect(debounced.isPending()).toBe(false);
77
- debounced("a");
78
- expect(debounced.isPending()).toBe(true);
79
- vi.advanceTimersByTime(100);
80
- expect(debounced.isPending()).toBe(false);
81
- });
82
- });
83
-
84
- describe("debouncedDispatch — leading", () => {
85
- it("fires immediately on first call", () => {
86
- const { action, run } = makeAction();
87
- const debounced = debouncedDispatch(action, { wait: 100, leading: true });
88
- debounced("first");
89
- expect(run).toHaveBeenCalledWith("first", expect.anything(), expect.anything());
90
- });
91
-
92
- it("suppresses calls within the cooldown window", () => {
93
- const { action, run } = makeAction();
94
- const debounced = debouncedDispatch(action, { wait: 100, leading: true });
95
- debounced("a");
96
- debounced("b");
97
- debounced("c");
98
- expect(run).toHaveBeenCalledTimes(1);
99
- expect(run).toHaveBeenCalledWith("a", expect.anything(), expect.anything());
100
- });
101
-
102
- it("fires trailing with last suppressed args after cooldown", () => {
103
- const { action, run } = makeAction();
104
- const debounced = debouncedDispatch(action, { wait: 100, leading: true });
105
- debounced("a");
106
- debounced("b");
107
- vi.advanceTimersByTime(100);
108
- expect(run).toHaveBeenCalledTimes(2);
109
- expect(run).toHaveBeenLastCalledWith("b", expect.anything(), expect.anything());
110
- });
111
-
112
- it("cancel after leading fire prevents trailing fire", () => {
113
- const { action, run } = makeAction();
114
- const debounced = debouncedDispatch(action, { wait: 100, leading: true });
115
- debounced("a");
116
- debounced("b");
117
- debounced.cancel();
118
- vi.advanceTimersByTime(200);
119
- expect(run).toHaveBeenCalledTimes(1);
120
- });
121
- });
@@ -1,133 +0,0 @@
1
- // @vitest-environment happy-dom
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
3
- import fc from "fast-check";
4
- vi.mock("./notifier.js", () => ({
5
- configure: vi.fn(),
6
- notifySuccess: vi.fn(),
7
- notifyError: vi.fn(),
8
- _resetNotifierForTest: vi.fn(),
9
- }));
10
- import { defineAction, _resetForTest as resetDefine } from "./define.js";
11
- import { _resetForTest as resetRegistry } from "./registry.js";
12
- import { _resetForTest as resetCleanup } from "./cleanup.js";
13
-
14
- beforeEach(() => {
15
- resetDefine();
16
- resetRegistry();
17
- resetCleanup();
18
- vi.clearAllMocks();
19
- });
20
-
21
- describe("scope serialization property", () => {
22
- it("for any N dispatches with same scope key, runs execute sequentially and all resolve", async () => {
23
- await fc.assert(
24
- fc.asyncProperty(fc.integer({ min: 2, max: 8 }), async (n) => {
25
- resetDefine();
26
- resetRegistry();
27
- resetCleanup();
28
- const timeline: { idx: number; event: "start" | "end" }[] = [];
29
- let counter = 0;
30
- const action = defineAction<number, number>({
31
- name: "prop.scope_serial",
32
- scope: "serial",
33
- error: false,
34
- run: async (args) => {
35
- const idx = args;
36
- timeline.push({ idx, event: "start" });
37
- await new Promise((r) => setTimeout(r, 1));
38
- timeline.push({ idx, event: "end" });
39
- return counter++;
40
- },
41
- });
42
- const promises = Array.from({ length: n }, (_, i) => action.dispatch(i));
43
- const results = await Promise.all(promises);
44
- expect(results).toHaveLength(n);
45
- for (const r of results) {
46
- expect(r).not.toBeNull();
47
- }
48
- for (let i = 0; i < timeline.length; i += 2) {
49
- expect(timeline[i]!.event).toBe("start");
50
- expect(timeline[i + 1]?.event).toBe("end");
51
- }
52
- }),
53
- { numRuns: 20 },
54
- );
55
- });
56
- });
57
-
58
- describe("dedupe property", () => {
59
- it("for any N dispatches with dedupe:true and same args, at most one run is in-flight", async () => {
60
- await fc.assert(
61
- fc.asyncProperty(fc.integer({ min: 2, max: 6 }), async (n) => {
62
- resetDefine();
63
- resetRegistry();
64
- resetCleanup();
65
- let inFlight = 0;
66
- let maxInFlight = 0;
67
- let runCount = 0;
68
- const action = defineAction<string, string>({
69
- name: "prop.dedupe",
70
- dedupe: true,
71
- error: false,
72
- run: async () => {
73
- inFlight++;
74
- runCount++;
75
- maxInFlight = Math.max(maxInFlight, inFlight);
76
- await new Promise((r) => setTimeout(r, 5));
77
- inFlight--;
78
- return "ok";
79
- },
80
- });
81
- const promises = Array.from({ length: n }, () => action.dispatch("same"));
82
- const results = await Promise.all(promises);
83
- expect(maxInFlight).toBe(1);
84
- for (const r of results) {
85
- expect(r).toBe("ok");
86
- }
87
- expect(runCount).toBe(1);
88
- }),
89
- { numRuns: 20 },
90
- );
91
- });
92
- });
93
-
94
- describe("cancellation property", () => {
95
- it("cancel during any lifecycle phase transitions to cancelled and returns null", async () => {
96
- await fc.assert(
97
- fc.asyncProperty(fc.constantFrom("immediate", "during-run"), async (phase) => {
98
- resetDefine();
99
- resetRegistry();
100
- resetCleanup();
101
- let runStarted = false;
102
- const action = defineAction<string, string>({
103
- name: "prop.cancel",
104
- error: false,
105
- run: async (_args, signal) => {
106
- runStarted = true;
107
- return new Promise<string>((_resolve, reject) => {
108
- signal.addEventListener(
109
- "abort",
110
- () => {
111
- reject(new DOMException("aborted", "AbortError"));
112
- },
113
- { once: true },
114
- );
115
- });
116
- },
117
- });
118
- if (phase === "immediate") {
119
- const p = action.dispatch("x");
120
- action.cancel();
121
- expect(await p).toBeNull();
122
- } else {
123
- const p = action.dispatch("x");
124
- await new Promise((r) => setTimeout(r, 1));
125
- expect(runStarted).toBe(true);
126
- action.cancel();
127
- expect(await p).toBeNull();
128
- }
129
- }),
130
- { numRuns: 15 },
131
- );
132
- });
133
- });