@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.
Files changed (151) hide show
  1. package/README.md +66 -267
  2. package/dist/common.types.d.ts +14 -14
  3. package/dist/errors/argument-error.d.ts +10 -10
  4. package/dist/errors/argument-error.d.ts.map +1 -1
  5. package/dist/errors/argument-error.js +2 -2
  6. package/dist/errors/argument-error.js.map +1 -1
  7. package/dist/errors/not-implemented-error.d.ts +8 -8
  8. package/dist/errors/not-implemented-error.js +2 -2
  9. package/dist/errors/not-implemented-error.js.map +1 -1
  10. package/dist/errors/sd-error.d.ts +10 -10
  11. package/dist/errors/sd-error.d.ts.map +1 -1
  12. package/dist/errors/timeout-error.d.ts +10 -10
  13. package/dist/errors/timeout-error.js +3 -3
  14. package/dist/errors/timeout-error.js.map +1 -1
  15. package/dist/extensions/arr-ext.d.ts +2 -2
  16. package/dist/extensions/arr-ext.helpers.d.ts +8 -8
  17. package/dist/extensions/arr-ext.helpers.js +1 -1
  18. package/dist/extensions/arr-ext.helpers.js.map +1 -1
  19. package/dist/extensions/arr-ext.js +13 -13
  20. package/dist/extensions/arr-ext.js.map +1 -1
  21. package/dist/extensions/arr-ext.types.d.ts +57 -57
  22. package/dist/extensions/arr-ext.types.d.ts.map +1 -1
  23. package/dist/extensions/map-ext.d.ts +16 -16
  24. package/dist/extensions/set-ext.d.ts +11 -11
  25. package/dist/features/debounce-queue.d.ts +17 -15
  26. package/dist/features/debounce-queue.d.ts.map +1 -1
  27. package/dist/features/debounce-queue.js +6 -6
  28. package/dist/features/debounce-queue.js.map +1 -1
  29. package/dist/features/event-emitter.d.ts +20 -20
  30. package/dist/features/event-emitter.js +17 -17
  31. package/dist/features/serial-queue.d.ts +11 -11
  32. package/dist/features/serial-queue.js +5 -5
  33. package/dist/features/serial-queue.js.map +1 -1
  34. package/dist/globals.d.ts +4 -4
  35. package/dist/types/date-only.d.ts +64 -64
  36. package/dist/types/date-only.d.ts.map +1 -1
  37. package/dist/types/date-only.js +63 -63
  38. package/dist/types/date-time.d.ts +37 -37
  39. package/dist/types/date-time.d.ts.map +1 -1
  40. package/dist/types/date-time.js +54 -37
  41. package/dist/types/date-time.js.map +1 -1
  42. package/dist/types/lazy-gc-map.d.ts +26 -26
  43. package/dist/types/lazy-gc-map.d.ts.map +1 -1
  44. package/dist/types/lazy-gc-map.js +26 -26
  45. package/dist/types/lazy-gc-map.js.map +1 -1
  46. package/dist/types/time.d.ts +25 -25
  47. package/dist/types/time.d.ts.map +1 -1
  48. package/dist/types/time.js +25 -25
  49. package/dist/types/time.js.map +1 -1
  50. package/dist/types/uuid.d.ts +11 -11
  51. package/dist/types/uuid.d.ts.map +1 -1
  52. package/dist/types/uuid.js +12 -12
  53. package/dist/types/uuid.js.map +1 -1
  54. package/dist/utils/bytes.d.ts +17 -17
  55. package/dist/utils/bytes.js +4 -4
  56. package/dist/utils/bytes.js.map +1 -1
  57. package/dist/utils/date-format.d.ts +45 -45
  58. package/dist/utils/date-format.js +1 -1
  59. package/dist/utils/date-format.js.map +1 -1
  60. package/dist/utils/error.d.ts +4 -4
  61. package/dist/utils/json.d.ts +17 -17
  62. package/dist/utils/json.js +3 -3
  63. package/dist/utils/json.js.map +1 -1
  64. package/dist/utils/num.d.ts +23 -23
  65. package/dist/utils/obj.d.ts +111 -111
  66. package/dist/utils/obj.d.ts.map +1 -1
  67. package/dist/utils/obj.js +3 -3
  68. package/dist/utils/obj.js.map +1 -1
  69. package/dist/utils/path.d.ts +10 -10
  70. package/dist/utils/primitive.d.ts +5 -5
  71. package/dist/utils/primitive.js +1 -1
  72. package/dist/utils/primitive.js.map +1 -1
  73. package/dist/utils/str.d.ts +46 -46
  74. package/dist/utils/str.d.ts.map +1 -1
  75. package/dist/utils/str.js +5 -5
  76. package/dist/utils/str.js.map +1 -1
  77. package/dist/utils/template-strings.d.ts +26 -26
  78. package/dist/utils/transferable.d.ts +18 -18
  79. package/dist/utils/transferable.js +1 -1
  80. package/dist/utils/transferable.js.map +1 -1
  81. package/dist/utils/wait.d.ts +9 -9
  82. package/dist/utils/xml.d.ts +13 -13
  83. package/dist/utils/xml.d.ts.map +1 -1
  84. package/dist/utils/xml.js +1 -0
  85. package/dist/utils/xml.js.map +1 -1
  86. package/dist/zip/sd-zip.d.ts +22 -22
  87. package/dist/zip/sd-zip.js +16 -16
  88. package/package.json +4 -4
  89. package/src/common.types.ts +17 -17
  90. package/src/errors/argument-error.ts +15 -15
  91. package/src/errors/not-implemented-error.ts +9 -9
  92. package/src/errors/sd-error.ts +12 -12
  93. package/src/errors/timeout-error.ts +12 -12
  94. package/src/extensions/arr-ext.helpers.ts +10 -10
  95. package/src/extensions/arr-ext.ts +57 -57
  96. package/src/extensions/arr-ext.types.ts +59 -59
  97. package/src/extensions/map-ext.ts +16 -16
  98. package/src/extensions/set-ext.ts +11 -11
  99. package/src/features/debounce-queue.ts +21 -19
  100. package/src/features/event-emitter.ts +25 -25
  101. package/src/features/serial-queue.ts +13 -13
  102. package/src/globals.ts +4 -4
  103. package/src/index.ts +1 -1
  104. package/src/types/date-only.ts +83 -83
  105. package/src/types/date-time.ts +64 -44
  106. package/src/types/lazy-gc-map.ts +45 -45
  107. package/src/types/time.ts +34 -34
  108. package/src/types/uuid.ts +17 -17
  109. package/src/utils/bytes.ts +35 -35
  110. package/src/utils/date-format.ts +65 -65
  111. package/src/utils/error.ts +4 -4
  112. package/src/utils/json.ts +39 -39
  113. package/src/utils/num.ts +23 -23
  114. package/src/utils/obj.ts +138 -138
  115. package/src/utils/path.ts +10 -10
  116. package/src/utils/primitive.ts +6 -6
  117. package/src/utils/str.ts +260 -261
  118. package/src/utils/template-strings.ts +29 -29
  119. package/src/utils/transferable.ts +284 -284
  120. package/src/utils/wait.ts +10 -10
  121. package/src/utils/xml.ts +20 -19
  122. package/src/zip/sd-zip.ts +25 -25
  123. package/tests/errors/errors.spec.ts +80 -0
  124. package/tests/extensions/array-extension.spec.ts +796 -0
  125. package/tests/extensions/map-extension.spec.ts +147 -0
  126. package/tests/extensions/set-extension.spec.ts +74 -0
  127. package/tests/types/date-only.spec.ts +638 -0
  128. package/tests/types/date-time.spec.ts +391 -0
  129. package/tests/types/lazy-gc-map.spec.ts +692 -0
  130. package/tests/types/time.spec.ts +559 -0
  131. package/tests/types/uuid.spec.ts +74 -0
  132. package/tests/utils/bytes-utils.spec.ts +230 -0
  133. package/tests/utils/date-format.spec.ts +373 -0
  134. package/tests/utils/debounce-queue.spec.ts +272 -0
  135. package/tests/utils/json.spec.ts +486 -0
  136. package/tests/utils/number.spec.ts +157 -0
  137. package/tests/utils/object.spec.ts +829 -0
  138. package/tests/utils/path.spec.ts +78 -0
  139. package/tests/utils/primitive.spec.ts +43 -0
  140. package/tests/utils/sd-event-emitter.spec.ts +216 -0
  141. package/tests/utils/serial-queue.spec.ts +365 -0
  142. package/tests/utils/string.spec.ts +281 -0
  143. package/tests/utils/template-strings.spec.ts +57 -0
  144. package/tests/utils/transferable.spec.ts +703 -0
  145. package/tests/utils/wait.spec.ts +145 -0
  146. package/tests/utils/xml.spec.ts +146 -0
  147. package/tests/zip/sd-zip.spec.ts +238 -0
  148. package/docs/extensions.md +0 -503
  149. package/docs/features.md +0 -109
  150. package/docs/types.md +0 -486
  151. 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
+ });