@simplysm/core-common 13.0.69 → 13.0.71
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/README.md +66 -267
- package/dist/common.types.d.ts +14 -14
- package/dist/errors/argument-error.d.ts +10 -10
- package/dist/errors/argument-error.d.ts.map +1 -1
- package/dist/errors/argument-error.js +2 -2
- package/dist/errors/argument-error.js.map +1 -1
- package/dist/errors/not-implemented-error.d.ts +8 -8
- package/dist/errors/not-implemented-error.js +2 -2
- package/dist/errors/not-implemented-error.js.map +1 -1
- package/dist/errors/sd-error.d.ts +10 -10
- package/dist/errors/sd-error.d.ts.map +1 -1
- package/dist/errors/timeout-error.d.ts +10 -10
- package/dist/errors/timeout-error.js +3 -3
- package/dist/errors/timeout-error.js.map +1 -1
- package/dist/extensions/arr-ext.d.ts +2 -2
- package/dist/extensions/arr-ext.helpers.d.ts +8 -8
- package/dist/extensions/arr-ext.helpers.js +1 -1
- package/dist/extensions/arr-ext.helpers.js.map +1 -1
- package/dist/extensions/arr-ext.js +13 -13
- package/dist/extensions/arr-ext.js.map +1 -1
- package/dist/extensions/arr-ext.types.d.ts +57 -57
- package/dist/extensions/arr-ext.types.d.ts.map +1 -1
- package/dist/extensions/map-ext.d.ts +16 -16
- package/dist/extensions/set-ext.d.ts +11 -11
- package/dist/features/debounce-queue.d.ts +17 -15
- package/dist/features/debounce-queue.d.ts.map +1 -1
- package/dist/features/debounce-queue.js +6 -6
- package/dist/features/debounce-queue.js.map +1 -1
- package/dist/features/event-emitter.d.ts +20 -20
- package/dist/features/event-emitter.js +17 -17
- package/dist/features/serial-queue.d.ts +11 -11
- package/dist/features/serial-queue.js +5 -5
- package/dist/features/serial-queue.js.map +1 -1
- package/dist/globals.d.ts +4 -4
- package/dist/types/date-only.d.ts +64 -64
- package/dist/types/date-only.d.ts.map +1 -1
- package/dist/types/date-only.js +63 -63
- package/dist/types/date-time.d.ts +37 -37
- package/dist/types/date-time.d.ts.map +1 -1
- package/dist/types/date-time.js +54 -37
- package/dist/types/date-time.js.map +1 -1
- package/dist/types/lazy-gc-map.d.ts +26 -26
- package/dist/types/lazy-gc-map.d.ts.map +1 -1
- package/dist/types/lazy-gc-map.js +26 -26
- package/dist/types/lazy-gc-map.js.map +1 -1
- package/dist/types/time.d.ts +25 -25
- package/dist/types/time.d.ts.map +1 -1
- package/dist/types/time.js +25 -25
- package/dist/types/time.js.map +1 -1
- package/dist/types/uuid.d.ts +11 -11
- package/dist/types/uuid.d.ts.map +1 -1
- package/dist/types/uuid.js +12 -12
- package/dist/types/uuid.js.map +1 -1
- package/dist/utils/bytes.d.ts +17 -17
- package/dist/utils/bytes.js +4 -4
- package/dist/utils/bytes.js.map +1 -1
- package/dist/utils/date-format.d.ts +45 -45
- package/dist/utils/date-format.js +1 -1
- package/dist/utils/date-format.js.map +1 -1
- package/dist/utils/error.d.ts +4 -4
- package/dist/utils/json.d.ts +17 -17
- package/dist/utils/json.js +3 -3
- package/dist/utils/json.js.map +1 -1
- package/dist/utils/num.d.ts +23 -23
- package/dist/utils/obj.d.ts +111 -111
- package/dist/utils/obj.d.ts.map +1 -1
- package/dist/utils/obj.js +3 -3
- package/dist/utils/obj.js.map +1 -1
- package/dist/utils/path.d.ts +10 -10
- package/dist/utils/primitive.d.ts +5 -5
- package/dist/utils/primitive.js +1 -1
- package/dist/utils/primitive.js.map +1 -1
- package/dist/utils/str.d.ts +46 -46
- package/dist/utils/str.d.ts.map +1 -1
- package/dist/utils/str.js +5 -5
- package/dist/utils/str.js.map +1 -1
- package/dist/utils/template-strings.d.ts +26 -26
- package/dist/utils/transferable.d.ts +18 -18
- package/dist/utils/transferable.js +1 -1
- package/dist/utils/transferable.js.map +1 -1
- package/dist/utils/wait.d.ts +9 -9
- package/dist/utils/xml.d.ts +13 -13
- package/dist/utils/xml.d.ts.map +1 -1
- package/dist/utils/xml.js +1 -0
- package/dist/utils/xml.js.map +1 -1
- package/dist/zip/sd-zip.d.ts +22 -22
- package/dist/zip/sd-zip.js +16 -16
- package/package.json +4 -4
- package/src/common.types.ts +17 -17
- package/src/errors/argument-error.ts +15 -15
- package/src/errors/not-implemented-error.ts +9 -9
- package/src/errors/sd-error.ts +12 -12
- package/src/errors/timeout-error.ts +12 -12
- package/src/extensions/arr-ext.helpers.ts +10 -10
- package/src/extensions/arr-ext.ts +57 -57
- package/src/extensions/arr-ext.types.ts +59 -59
- package/src/extensions/map-ext.ts +16 -16
- package/src/extensions/set-ext.ts +11 -11
- package/src/features/debounce-queue.ts +21 -19
- package/src/features/event-emitter.ts +25 -25
- package/src/features/serial-queue.ts +13 -13
- package/src/globals.ts +4 -4
- package/src/index.ts +1 -1
- package/src/types/date-only.ts +83 -83
- package/src/types/date-time.ts +64 -44
- package/src/types/lazy-gc-map.ts +45 -45
- package/src/types/time.ts +34 -34
- package/src/types/uuid.ts +17 -17
- package/src/utils/bytes.ts +35 -35
- package/src/utils/date-format.ts +65 -65
- package/src/utils/error.ts +4 -4
- package/src/utils/json.ts +39 -39
- package/src/utils/num.ts +23 -23
- package/src/utils/obj.ts +138 -138
- package/src/utils/path.ts +10 -10
- package/src/utils/primitive.ts +6 -6
- package/src/utils/str.ts +260 -261
- package/src/utils/template-strings.ts +29 -29
- package/src/utils/transferable.ts +284 -284
- package/src/utils/wait.ts +10 -10
- package/src/utils/xml.ts +20 -19
- package/src/zip/sd-zip.ts +25 -25
- package/tests/errors/errors.spec.ts +80 -0
- package/tests/extensions/array-extension.spec.ts +796 -0
- package/tests/extensions/map-extension.spec.ts +147 -0
- package/tests/extensions/set-extension.spec.ts +74 -0
- package/tests/types/date-only.spec.ts +638 -0
- package/tests/types/date-time.spec.ts +391 -0
- package/tests/types/lazy-gc-map.spec.ts +692 -0
- package/tests/types/time.spec.ts +559 -0
- package/tests/types/uuid.spec.ts +74 -0
- package/tests/utils/bytes-utils.spec.ts +230 -0
- package/tests/utils/date-format.spec.ts +373 -0
- package/tests/utils/debounce-queue.spec.ts +272 -0
- package/tests/utils/json.spec.ts +486 -0
- package/tests/utils/number.spec.ts +157 -0
- package/tests/utils/object.spec.ts +829 -0
- package/tests/utils/path.spec.ts +78 -0
- package/tests/utils/primitive.spec.ts +43 -0
- package/tests/utils/sd-event-emitter.spec.ts +216 -0
- package/tests/utils/serial-queue.spec.ts +365 -0
- package/tests/utils/string.spec.ts +281 -0
- package/tests/utils/template-strings.spec.ts +57 -0
- package/tests/utils/transferable.spec.ts +703 -0
- package/tests/utils/wait.spec.ts +145 -0
- package/tests/utils/xml.spec.ts +146 -0
- package/tests/zip/sd-zip.spec.ts +238 -0
- package/docs/extensions.md +0 -503
- package/docs/features.md +0 -109
- package/docs/types.md +0 -486
- package/docs/utils.md +0 -780
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { pathJoin, pathBasename, pathExtname } from "@simplysm/core-common";
|
|
3
|
+
|
|
4
|
+
describe("path utils", () => {
|
|
5
|
+
describe("pathJoin()", () => {
|
|
6
|
+
it("Combines path segments", () => {
|
|
7
|
+
expect(pathJoin("a", "b", "c")).toBe("a/b/c");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("Preserves leading slash", () => {
|
|
11
|
+
expect(pathJoin("/a", "b")).toBe("/a/b");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("Removes duplicate slashes", () => {
|
|
15
|
+
expect(pathJoin("a/", "/b/", "/c")).toBe("a/b/c");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("Ignores empty segments", () => {
|
|
19
|
+
expect(pathJoin("a", "", "b")).toBe("a/b");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("Returns single segment", () => {
|
|
23
|
+
expect(pathJoin("a")).toBe("a");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("Empty input returns empty string", () => {
|
|
27
|
+
expect(pathJoin()).toBe("");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("pathBasename()", () => {
|
|
32
|
+
it("Extracts filename", () => {
|
|
33
|
+
expect(pathBasename("a/b/file.txt")).toBe("file.txt");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("Removes extension", () => {
|
|
37
|
+
expect(pathBasename("a/b/file.txt", ".txt")).toBe("file");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("Ignores non-matching extension", () => {
|
|
41
|
+
expect(pathBasename("a/b/file.txt", ".md")).toBe("file.txt");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("Handles filename without path", () => {
|
|
45
|
+
expect(pathBasename("file.txt")).toBe("file.txt");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("Empty string returns empty string", () => {
|
|
49
|
+
expect(pathBasename("")).toBe("");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("pathExtname()", () => {
|
|
54
|
+
it("Extracts extension", () => {
|
|
55
|
+
expect(pathExtname("file.txt")).toBe(".txt");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("Extracts only last extension", () => {
|
|
59
|
+
expect(pathExtname("archive.tar.gz")).toBe(".gz");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("File without extension returns empty string", () => {
|
|
63
|
+
expect(pathExtname("Makefile")).toBe("");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("Hidden file returns empty string", () => {
|
|
67
|
+
expect(pathExtname(".gitignore")).toBe("");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("Extracts extension from file with path", () => {
|
|
71
|
+
expect(pathExtname("a/b/file.ts")).toBe(".ts");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("Empty string returns empty string", () => {
|
|
75
|
+
expect(pathExtname("")).toBe("");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getPrimitiveTypeStr, DateTime, DateOnly, Time, Uuid } from "@simplysm/core-common";
|
|
3
|
+
|
|
4
|
+
describe("primitive utils", () => {
|
|
5
|
+
describe("getPrimitiveTypeStr()", () => {
|
|
6
|
+
it("Returns string", () => {
|
|
7
|
+
expect(getPrimitiveTypeStr("hello")).toBe("string");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("Returns number", () => {
|
|
11
|
+
expect(getPrimitiveTypeStr(42)).toBe("number");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("Returns boolean", () => {
|
|
15
|
+
expect(getPrimitiveTypeStr(true)).toBe("boolean");
|
|
16
|
+
expect(getPrimitiveTypeStr(false)).toBe("boolean");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("Returns DateTime", () => {
|
|
20
|
+
expect(getPrimitiveTypeStr(new DateTime())).toBe("DateTime");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("Returns DateOnly", () => {
|
|
24
|
+
expect(getPrimitiveTypeStr(new DateOnly())).toBe("DateOnly");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("Returns Time", () => {
|
|
28
|
+
expect(getPrimitiveTypeStr(new Time())).toBe("Time");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("Returns Uuid", () => {
|
|
32
|
+
expect(getPrimitiveTypeStr(Uuid.new())).toBe("Uuid");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("Uint8Array returns Bytes", () => {
|
|
36
|
+
expect(getPrimitiveTypeStr(new Uint8Array([1, 2]))).toBe("Bytes");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("Unsupported type throws error", () => {
|
|
40
|
+
expect(() => getPrimitiveTypeStr({} as never)).toThrow();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { EventEmitter } from "@simplysm/core-common";
|
|
3
|
+
|
|
4
|
+
interface TestEvents {
|
|
5
|
+
message: string;
|
|
6
|
+
count: number;
|
|
7
|
+
data: { id: number; name: string };
|
|
8
|
+
empty: void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("SdEventEmitter", () => {
|
|
12
|
+
//#region on/emit
|
|
13
|
+
|
|
14
|
+
describe("on() / emit()", () => {
|
|
15
|
+
it("Emits and receives event", () => {
|
|
16
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
17
|
+
const listener = vi.fn();
|
|
18
|
+
|
|
19
|
+
emitter.on("message", listener);
|
|
20
|
+
emitter.emit("message", "hello");
|
|
21
|
+
|
|
22
|
+
expect(listener).toHaveBeenCalledWith("hello");
|
|
23
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("Listener called multiple times on multiple emit", () => {
|
|
27
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
28
|
+
const listener = vi.fn();
|
|
29
|
+
|
|
30
|
+
emitter.on("count", listener);
|
|
31
|
+
emitter.emit("count", 1);
|
|
32
|
+
emitter.emit("count", 2);
|
|
33
|
+
emitter.emit("count", 3);
|
|
34
|
+
|
|
35
|
+
expect(listener).toHaveBeenCalledTimes(3);
|
|
36
|
+
expect(listener).toHaveBeenNthCalledWith(1, 1);
|
|
37
|
+
expect(listener).toHaveBeenNthCalledWith(2, 2);
|
|
38
|
+
expect(listener).toHaveBeenNthCalledWith(3, 3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("Passes object data", () => {
|
|
42
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
43
|
+
const listener = vi.fn();
|
|
44
|
+
|
|
45
|
+
emitter.on("data", listener);
|
|
46
|
+
emitter.emit("data", { id: 1, name: "test" });
|
|
47
|
+
|
|
48
|
+
expect(listener).toHaveBeenCalledWith({ id: 1, name: "test" });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Handles void event", () => {
|
|
52
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
53
|
+
const listener = vi.fn();
|
|
54
|
+
|
|
55
|
+
emitter.on("empty", listener);
|
|
56
|
+
emitter.emit("empty");
|
|
57
|
+
|
|
58
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("Can register multiple listeners for same event", () => {
|
|
62
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
63
|
+
const listener1 = vi.fn();
|
|
64
|
+
const listener2 = vi.fn();
|
|
65
|
+
|
|
66
|
+
emitter.on("message", listener1);
|
|
67
|
+
emitter.on("message", listener2);
|
|
68
|
+
emitter.emit("message", "test");
|
|
69
|
+
|
|
70
|
+
expect(listener1).toHaveBeenCalledWith("test");
|
|
71
|
+
expect(listener2).toHaveBeenCalledWith("test");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
|
|
77
|
+
//#region off
|
|
78
|
+
|
|
79
|
+
describe("off()", () => {
|
|
80
|
+
it("Removes listener", () => {
|
|
81
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
82
|
+
const listener = vi.fn();
|
|
83
|
+
|
|
84
|
+
emitter.on("message", listener);
|
|
85
|
+
emitter.off("message", listener);
|
|
86
|
+
emitter.emit("message", "test");
|
|
87
|
+
|
|
88
|
+
expect(listener).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("Removes only specified listener, others remain", () => {
|
|
92
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
93
|
+
const listener1 = vi.fn();
|
|
94
|
+
const listener2 = vi.fn();
|
|
95
|
+
|
|
96
|
+
emitter.on("message", listener1);
|
|
97
|
+
emitter.on("message", listener2);
|
|
98
|
+
emitter.off("message", listener1);
|
|
99
|
+
emitter.emit("message", "test");
|
|
100
|
+
|
|
101
|
+
expect(listener1).not.toHaveBeenCalled();
|
|
102
|
+
expect(listener2).toHaveBeenCalledWith("test");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("No error removing unregistered listener", () => {
|
|
106
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
107
|
+
const listener = vi.fn();
|
|
108
|
+
|
|
109
|
+
// Should run without error
|
|
110
|
+
expect(() => {
|
|
111
|
+
emitter.off("message", listener);
|
|
112
|
+
}).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
|
|
118
|
+
//#region listenerCount
|
|
119
|
+
|
|
120
|
+
describe("listenerCount()", () => {
|
|
121
|
+
it("Accurately counts listeners", () => {
|
|
122
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
123
|
+
|
|
124
|
+
expect(emitter.listenerCount("message")).toBe(0);
|
|
125
|
+
|
|
126
|
+
emitter.on("message", () => {});
|
|
127
|
+
expect(emitter.listenerCount("message")).toBe(1);
|
|
128
|
+
|
|
129
|
+
emitter.on("message", () => {});
|
|
130
|
+
expect(emitter.listenerCount("message")).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("Count decreases after off", () => {
|
|
134
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
135
|
+
const listener = vi.fn();
|
|
136
|
+
|
|
137
|
+
emitter.on("message", listener);
|
|
138
|
+
expect(emitter.listenerCount("message")).toBe(1);
|
|
139
|
+
|
|
140
|
+
emitter.off("message", listener);
|
|
141
|
+
expect(emitter.listenerCount("message")).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("Unregistered event type has 0 count", () => {
|
|
145
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
146
|
+
|
|
147
|
+
expect(emitter.listenerCount("message")).toBe(0);
|
|
148
|
+
expect(emitter.listenerCount("count")).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("Different event types have independent counts", () => {
|
|
152
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
153
|
+
|
|
154
|
+
emitter.on("message", () => {});
|
|
155
|
+
emitter.on("message", () => {});
|
|
156
|
+
emitter.on("count", () => {});
|
|
157
|
+
|
|
158
|
+
expect(emitter.listenerCount("message")).toBe(2);
|
|
159
|
+
expect(emitter.listenerCount("count")).toBe(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
|
|
165
|
+
//#region Prevent duplicate registration
|
|
166
|
+
|
|
167
|
+
describe("Prevent duplicate registration", () => {
|
|
168
|
+
it("Duplicate registration of same listener is ignored", () => {
|
|
169
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
170
|
+
const listener = vi.fn();
|
|
171
|
+
|
|
172
|
+
emitter.on("message", listener);
|
|
173
|
+
emitter.on("message", listener); // duplicate registration attempt
|
|
174
|
+
emitter.on("message", listener); // duplicate registration attempt
|
|
175
|
+
|
|
176
|
+
expect(emitter.listenerCount("message")).toBe(1);
|
|
177
|
+
|
|
178
|
+
emitter.emit("message", "test");
|
|
179
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("Can register same listener to different events", () => {
|
|
183
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
184
|
+
const listener = vi.fn();
|
|
185
|
+
|
|
186
|
+
emitter.on("message", listener);
|
|
187
|
+
emitter.on("count", listener as any);
|
|
188
|
+
|
|
189
|
+
expect(emitter.listenerCount("message")).toBe(1);
|
|
190
|
+
expect(emitter.listenerCount("count")).toBe(1);
|
|
191
|
+
|
|
192
|
+
emitter.emit("message", "test");
|
|
193
|
+
emitter.emit("count", 123);
|
|
194
|
+
|
|
195
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
196
|
+
expect(listener).toHaveBeenNthCalledWith(1, "test");
|
|
197
|
+
expect(listener).toHaveBeenNthCalledWith(2, 123);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("Single off call removes after duplicate registration", () => {
|
|
201
|
+
const emitter = new EventEmitter<TestEvents>();
|
|
202
|
+
const listener = vi.fn();
|
|
203
|
+
|
|
204
|
+
emitter.on("message", listener);
|
|
205
|
+
emitter.on("message", listener); // duplicate registration attempt
|
|
206
|
+
|
|
207
|
+
emitter.off("message", listener);
|
|
208
|
+
expect(emitter.listenerCount("message")).toBe(0);
|
|
209
|
+
|
|
210
|
+
emitter.emit("message", "test");
|
|
211
|
+
expect(listener).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
});
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { SerialQueue, SdError } from "@simplysm/core-common";
|
|
3
|
+
|
|
4
|
+
describe("SerialQueue", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
//#region Sequential execution
|
|
14
|
+
|
|
15
|
+
describe("Sequential execution", () => {
|
|
16
|
+
it("Executes queued functions in order", async () => {
|
|
17
|
+
const queue = new SerialQueue();
|
|
18
|
+
const calls: number[] = [];
|
|
19
|
+
|
|
20
|
+
queue.run(() => {
|
|
21
|
+
calls.push(1);
|
|
22
|
+
});
|
|
23
|
+
queue.run(() => {
|
|
24
|
+
calls.push(2);
|
|
25
|
+
});
|
|
26
|
+
queue.run(() => {
|
|
27
|
+
calls.push(3);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
31
|
+
|
|
32
|
+
expect(calls).toEqual([1, 2, 3]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("Executes next task after previous task completes", async () => {
|
|
36
|
+
const queue = new SerialQueue();
|
|
37
|
+
const calls: number[] = [];
|
|
38
|
+
const timestamps: number[] = [];
|
|
39
|
+
|
|
40
|
+
queue.run(async () => {
|
|
41
|
+
timestamps.push(Date.now());
|
|
42
|
+
calls.push(1);
|
|
43
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
44
|
+
});
|
|
45
|
+
queue.run(async () => {
|
|
46
|
+
timestamps.push(Date.now());
|
|
47
|
+
calls.push(2);
|
|
48
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
52
|
+
|
|
53
|
+
expect(calls).toEqual([1, 2]);
|
|
54
|
+
// Second task starts after first task completes (exactly 50ms)
|
|
55
|
+
expect(timestamps[1] - timestamps[0]).toBe(50);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("Executes tasks added during execution sequentially", async () => {
|
|
59
|
+
const queue = new SerialQueue();
|
|
60
|
+
const calls: number[] = [];
|
|
61
|
+
|
|
62
|
+
queue.run(async () => {
|
|
63
|
+
calls.push(1);
|
|
64
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Wait for first task to start
|
|
68
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
69
|
+
|
|
70
|
+
// Add task during execution
|
|
71
|
+
queue.run(() => {
|
|
72
|
+
calls.push(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
76
|
+
|
|
77
|
+
expect(calls).toEqual([1, 2]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
|
|
83
|
+
//#region Gap interval
|
|
84
|
+
|
|
85
|
+
describe("Gap interval", () => {
|
|
86
|
+
it("Waits gap duration between tasks when set", async () => {
|
|
87
|
+
const queue = new SerialQueue(50);
|
|
88
|
+
const timestamps: number[] = [];
|
|
89
|
+
|
|
90
|
+
queue.run(() => {
|
|
91
|
+
timestamps.push(Date.now());
|
|
92
|
+
});
|
|
93
|
+
queue.run(() => {
|
|
94
|
+
timestamps.push(Date.now());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
98
|
+
|
|
99
|
+
expect(timestamps).toHaveLength(2);
|
|
100
|
+
// Exactly 50ms gap between two tasks
|
|
101
|
+
expect(timestamps[1] - timestamps[0]).toBe(50);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("Executes next task immediately when gap is 0", async () => {
|
|
105
|
+
const queue = new SerialQueue(0);
|
|
106
|
+
const timestamps: number[] = [];
|
|
107
|
+
|
|
108
|
+
queue.run(() => {
|
|
109
|
+
timestamps.push(Date.now());
|
|
110
|
+
});
|
|
111
|
+
queue.run(() => {
|
|
112
|
+
timestamps.push(Date.now());
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
116
|
+
|
|
117
|
+
expect(timestamps).toHaveLength(2);
|
|
118
|
+
// Gap is 0
|
|
119
|
+
expect(timestamps[1] - timestamps[0]).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("Default gap is 0", async () => {
|
|
123
|
+
const queue = new SerialQueue();
|
|
124
|
+
const timestamps: number[] = [];
|
|
125
|
+
|
|
126
|
+
queue.run(() => {
|
|
127
|
+
timestamps.push(Date.now());
|
|
128
|
+
});
|
|
129
|
+
queue.run(() => {
|
|
130
|
+
timestamps.push(Date.now());
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
134
|
+
|
|
135
|
+
expect(timestamps).toHaveLength(2);
|
|
136
|
+
expect(timestamps[1] - timestamps[0]).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("Does not wait gap after the last task", async () => {
|
|
140
|
+
const queue = new SerialQueue(100);
|
|
141
|
+
const timestamps: number[] = [];
|
|
142
|
+
|
|
143
|
+
queue.run(() => {
|
|
144
|
+
timestamps.push(Date.now());
|
|
145
|
+
});
|
|
146
|
+
queue.run(() => {
|
|
147
|
+
timestamps.push(Date.now());
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Wait long enough (task1 + gap100 + task2)
|
|
151
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
152
|
+
|
|
153
|
+
// Verify gap was applied (exactly 100ms)
|
|
154
|
+
expect(timestamps).toHaveLength(2);
|
|
155
|
+
expect(timestamps[1] - timestamps[0]).toBe(100);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
|
|
161
|
+
//#region Error handling
|
|
162
|
+
|
|
163
|
+
describe("Error handling", () => {
|
|
164
|
+
it("Emits error event when error occurs", async () => {
|
|
165
|
+
const queue = new SerialQueue();
|
|
166
|
+
const errors: SdError[] = [];
|
|
167
|
+
|
|
168
|
+
queue.on("error", (err) => {
|
|
169
|
+
errors.push(err);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
queue.run(() => {
|
|
173
|
+
throw new Error("test error");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
177
|
+
|
|
178
|
+
expect(errors).toHaveLength(1);
|
|
179
|
+
expect(errors[0]).toBeInstanceOf(SdError);
|
|
180
|
+
expect(errors[0].message).toContain("Error occurred while executing queue task");
|
|
181
|
+
expect(errors[0].message).toContain("test error");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("Continues executing next task even after error", async () => {
|
|
185
|
+
const queue = new SerialQueue();
|
|
186
|
+
const calls: number[] = [];
|
|
187
|
+
const errors: SdError[] = [];
|
|
188
|
+
|
|
189
|
+
// Add error listener to prevent unhandled rejection
|
|
190
|
+
queue.on("error", (err) => {
|
|
191
|
+
errors.push(err);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
queue.run(() => {
|
|
195
|
+
calls.push(1);
|
|
196
|
+
throw new Error("error");
|
|
197
|
+
});
|
|
198
|
+
queue.run(() => {
|
|
199
|
+
calls.push(2);
|
|
200
|
+
});
|
|
201
|
+
queue.run(() => {
|
|
202
|
+
calls.push(3);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
206
|
+
|
|
207
|
+
expect(calls).toEqual([1, 2, 3]);
|
|
208
|
+
expect(errors).toHaveLength(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("Executes all tasks even if multiple errors occur", async () => {
|
|
212
|
+
const queue = new SerialQueue();
|
|
213
|
+
const calls: number[] = [];
|
|
214
|
+
const errors: SdError[] = [];
|
|
215
|
+
|
|
216
|
+
queue.on("error", (err) => {
|
|
217
|
+
errors.push(err);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
queue.run(() => {
|
|
221
|
+
calls.push(1);
|
|
222
|
+
throw new Error("error 1");
|
|
223
|
+
});
|
|
224
|
+
queue.run(() => {
|
|
225
|
+
calls.push(2);
|
|
226
|
+
});
|
|
227
|
+
queue.run(() => {
|
|
228
|
+
calls.push(3);
|
|
229
|
+
throw new Error("error 3");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
233
|
+
|
|
234
|
+
expect(calls).toEqual([1, 2, 3]);
|
|
235
|
+
expect(errors).toHaveLength(2);
|
|
236
|
+
expect(errors[0].message).toContain("error 1");
|
|
237
|
+
expect(errors[1].message).toContain("error 3");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
|
|
243
|
+
//#region dispose
|
|
244
|
+
|
|
245
|
+
describe("dispose()", () => {
|
|
246
|
+
it("Clears pending queue", async () => {
|
|
247
|
+
const queue = new SerialQueue();
|
|
248
|
+
const calls: number[] = [];
|
|
249
|
+
|
|
250
|
+
// First task is executing
|
|
251
|
+
queue.run(async () => {
|
|
252
|
+
calls.push(1);
|
|
253
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Add pending tasks
|
|
257
|
+
queue.run(() => {
|
|
258
|
+
calls.push(2);
|
|
259
|
+
});
|
|
260
|
+
queue.run(() => {
|
|
261
|
+
calls.push(3);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Call dispose after first task starts
|
|
265
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
266
|
+
queue.dispose();
|
|
267
|
+
|
|
268
|
+
// Wait for all tasks to complete
|
|
269
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
270
|
+
|
|
271
|
+
// Only first task executes (running tasks complete)
|
|
272
|
+
expect(calls).toEqual([1]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("New tasks execute normally after dispose", async () => {
|
|
276
|
+
const queue = new SerialQueue();
|
|
277
|
+
const calls: number[] = [];
|
|
278
|
+
|
|
279
|
+
queue.run(() => {
|
|
280
|
+
calls.push(1);
|
|
281
|
+
});
|
|
282
|
+
queue.dispose();
|
|
283
|
+
|
|
284
|
+
// Add new task after dispose
|
|
285
|
+
queue.run(() => {
|
|
286
|
+
calls.push(2);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
290
|
+
|
|
291
|
+
expect(calls).toContain(2);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("Safe to call multiple times", () => {
|
|
295
|
+
const queue = new SerialQueue();
|
|
296
|
+
|
|
297
|
+
// Multiple calls without error
|
|
298
|
+
queue.dispose();
|
|
299
|
+
queue.dispose();
|
|
300
|
+
queue.dispose();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("Automatically disposes with using statement", async () => {
|
|
304
|
+
const calls: number[] = [];
|
|
305
|
+
{
|
|
306
|
+
using queue = new SerialQueue();
|
|
307
|
+
queue.run(async () => {
|
|
308
|
+
calls.push(1);
|
|
309
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
310
|
+
});
|
|
311
|
+
queue.run(() => {
|
|
312
|
+
calls.push(2);
|
|
313
|
+
});
|
|
314
|
+
await vi.advanceTimersByTimeAsync(20);
|
|
315
|
+
} // dispose called automatically when using block ends
|
|
316
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
317
|
+
// First task (running) completes, but pending tasks don't execute
|
|
318
|
+
expect(calls).toEqual([1]);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
//#endregion
|
|
323
|
+
|
|
324
|
+
//#region Synchronous function support
|
|
325
|
+
|
|
326
|
+
describe("Synchronous function support", () => {
|
|
327
|
+
it("Can execute synchronous functions", async () => {
|
|
328
|
+
const queue = new SerialQueue();
|
|
329
|
+
const calls: number[] = [];
|
|
330
|
+
|
|
331
|
+
queue.run(() => {
|
|
332
|
+
calls.push(1);
|
|
333
|
+
});
|
|
334
|
+
queue.run(() => {
|
|
335
|
+
calls.push(2);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
339
|
+
|
|
340
|
+
expect(calls).toEqual([1, 2]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("Can mix synchronous and asynchronous functions", async () => {
|
|
344
|
+
const queue = new SerialQueue();
|
|
345
|
+
const calls: number[] = [];
|
|
346
|
+
|
|
347
|
+
queue.run(() => {
|
|
348
|
+
calls.push(1);
|
|
349
|
+
});
|
|
350
|
+
queue.run(async () => {
|
|
351
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
352
|
+
calls.push(2);
|
|
353
|
+
});
|
|
354
|
+
queue.run(() => {
|
|
355
|
+
calls.push(3);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
359
|
+
|
|
360
|
+
expect(calls).toEqual([1, 2, 3]);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
//#endregion
|
|
365
|
+
});
|