@bluelibs/runner 2.2.3 → 3.0.0

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 (204) hide show
  1. package/README.md +1315 -942
  2. package/dist/common.types.d.ts +20 -0
  3. package/dist/common.types.js +4 -0
  4. package/dist/common.types.js.map +1 -0
  5. package/dist/context.d.ts +34 -0
  6. package/dist/context.js +58 -0
  7. package/dist/context.js.map +1 -0
  8. package/dist/define.d.ts +22 -3
  9. package/dist/define.js +52 -8
  10. package/dist/define.js.map +1 -1
  11. package/dist/defs.d.ts +52 -31
  12. package/dist/defs.js +10 -2
  13. package/dist/defs.js.map +1 -1
  14. package/dist/errors.js +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/event.types.d.ts +18 -0
  17. package/dist/event.types.js +4 -0
  18. package/dist/event.types.js.map +1 -0
  19. package/dist/examples/registrator-example.d.ts +122 -0
  20. package/dist/examples/registrator-example.js +147 -0
  21. package/dist/examples/registrator-example.js.map +1 -0
  22. package/dist/globals/globalEvents.d.ts +41 -0
  23. package/dist/globals/globalEvents.js +94 -0
  24. package/dist/globals/globalEvents.js.map +1 -0
  25. package/dist/globals/globalMiddleware.d.ts +23 -0
  26. package/dist/globals/globalMiddleware.js +15 -0
  27. package/dist/globals/globalMiddleware.js.map +1 -0
  28. package/dist/globals/globalResources.d.ts +27 -0
  29. package/dist/globals/globalResources.js +47 -0
  30. package/dist/globals/globalResources.js.map +1 -0
  31. package/dist/globals/middleware/cache.middleware.d.ts +34 -0
  32. package/dist/globals/middleware/cache.middleware.js +85 -0
  33. package/dist/globals/middleware/cache.middleware.js.map +1 -0
  34. package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
  35. package/dist/globals/middleware/requireContext.middleware.js +25 -0
  36. package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
  37. package/dist/globals/middleware/retry.middleware.d.ts +20 -0
  38. package/dist/globals/middleware/retry.middleware.js +34 -0
  39. package/dist/globals/middleware/retry.middleware.js.map +1 -0
  40. package/dist/globals/resources/queue.resource.d.ts +7 -0
  41. package/dist/globals/resources/queue.resource.js +31 -0
  42. package/dist/globals/resources/queue.resource.js.map +1 -0
  43. package/dist/index.d.ts +45 -9
  44. package/dist/index.js +14 -9
  45. package/dist/index.js.map +1 -1
  46. package/dist/middleware.types.d.ts +40 -0
  47. package/dist/middleware.types.js +4 -0
  48. package/dist/middleware.types.js.map +1 -0
  49. package/dist/models/DependencyProcessor.d.ts +2 -1
  50. package/dist/models/DependencyProcessor.js +11 -13
  51. package/dist/models/DependencyProcessor.js.map +1 -1
  52. package/dist/models/EventManager.d.ts +5 -0
  53. package/dist/models/EventManager.js +44 -2
  54. package/dist/models/EventManager.js.map +1 -1
  55. package/dist/models/Logger.d.ts +30 -12
  56. package/dist/models/Logger.js +130 -42
  57. package/dist/models/Logger.js.map +1 -1
  58. package/dist/models/OverrideManager.d.ts +13 -0
  59. package/dist/models/OverrideManager.js +70 -0
  60. package/dist/models/OverrideManager.js.map +1 -0
  61. package/dist/models/Queue.d.ts +25 -0
  62. package/dist/models/Queue.js +54 -0
  63. package/dist/models/Queue.js.map +1 -0
  64. package/dist/models/ResourceInitializer.d.ts +5 -2
  65. package/dist/models/ResourceInitializer.js +20 -14
  66. package/dist/models/ResourceInitializer.js.map +1 -1
  67. package/dist/models/Semaphore.d.ts +61 -0
  68. package/dist/models/Semaphore.js +166 -0
  69. package/dist/models/Semaphore.js.map +1 -0
  70. package/dist/models/Store.d.ts +17 -72
  71. package/dist/models/Store.js +71 -269
  72. package/dist/models/Store.js.map +1 -1
  73. package/dist/models/StoreConstants.d.ts +11 -0
  74. package/dist/models/StoreConstants.js +18 -0
  75. package/dist/models/StoreConstants.js.map +1 -0
  76. package/dist/models/StoreRegistry.d.ts +25 -0
  77. package/dist/models/StoreRegistry.js +171 -0
  78. package/dist/models/StoreRegistry.js.map +1 -0
  79. package/dist/models/StoreTypes.d.ts +21 -0
  80. package/dist/models/StoreTypes.js +3 -0
  81. package/dist/models/StoreTypes.js.map +1 -0
  82. package/dist/models/StoreValidator.d.ts +10 -0
  83. package/dist/models/StoreValidator.js +41 -0
  84. package/dist/models/StoreValidator.js.map +1 -0
  85. package/dist/models/TaskRunner.js +39 -24
  86. package/dist/models/TaskRunner.js.map +1 -1
  87. package/dist/models/VarStore.d.ts +17 -0
  88. package/dist/models/VarStore.js +60 -0
  89. package/dist/models/VarStore.js.map +1 -0
  90. package/dist/models/index.d.ts +3 -0
  91. package/dist/models/index.js +3 -0
  92. package/dist/models/index.js.map +1 -1
  93. package/dist/resource.types.d.ts +31 -0
  94. package/dist/resource.types.js +3 -0
  95. package/dist/resource.types.js.map +1 -0
  96. package/dist/run.d.ts +4 -1
  97. package/dist/run.js +6 -3
  98. package/dist/run.js.map +1 -1
  99. package/dist/symbols.d.ts +24 -0
  100. package/dist/symbols.js +29 -0
  101. package/dist/symbols.js.map +1 -0
  102. package/dist/task.types.d.ts +55 -0
  103. package/dist/task.types.js +23 -0
  104. package/dist/task.types.js.map +1 -0
  105. package/dist/tools/registratorId.d.ts +4 -0
  106. package/dist/tools/registratorId.js +40 -0
  107. package/dist/tools/registratorId.js.map +1 -0
  108. package/dist/tools/simpleHash.d.ts +9 -0
  109. package/dist/tools/simpleHash.js +34 -0
  110. package/dist/tools/simpleHash.js.map +1 -0
  111. package/dist/types/base-interfaces.d.ts +18 -0
  112. package/dist/types/base-interfaces.js +6 -0
  113. package/dist/types/base-interfaces.js.map +1 -0
  114. package/dist/types/base.d.ts +13 -0
  115. package/dist/types/base.js +3 -0
  116. package/dist/types/base.js.map +1 -0
  117. package/dist/types/dependencies.d.ts +22 -0
  118. package/dist/types/dependencies.js +3 -0
  119. package/dist/types/dependencies.js.map +1 -0
  120. package/dist/types/dependency-core.d.ts +14 -0
  121. package/dist/types/dependency-core.js +5 -0
  122. package/dist/types/dependency-core.js.map +1 -0
  123. package/dist/types/events.d.ts +52 -0
  124. package/dist/types/events.js +6 -0
  125. package/dist/types/events.js.map +1 -0
  126. package/dist/types/hooks.d.ts +16 -0
  127. package/dist/types/hooks.js +5 -0
  128. package/dist/types/hooks.js.map +1 -0
  129. package/dist/types/index.d.ts +14 -0
  130. package/dist/types/index.js +27 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/meta.d.ts +13 -0
  133. package/dist/types/meta.js +5 -0
  134. package/dist/types/meta.js.map +1 -0
  135. package/dist/types/middleware.d.ts +38 -0
  136. package/dist/types/middleware.js +6 -0
  137. package/dist/types/middleware.js.map +1 -0
  138. package/dist/types/registerable.d.ts +10 -0
  139. package/dist/types/registerable.js +5 -0
  140. package/dist/types/registerable.js.map +1 -0
  141. package/dist/types/resources.d.ts +44 -0
  142. package/dist/types/resources.js +5 -0
  143. package/dist/types/resources.js.map +1 -0
  144. package/dist/types/symbols.d.ts +24 -0
  145. package/dist/types/symbols.js +30 -0
  146. package/dist/types/symbols.js.map +1 -0
  147. package/dist/types/tasks.d.ts +41 -0
  148. package/dist/types/tasks.js +5 -0
  149. package/dist/types/tasks.js.map +1 -0
  150. package/dist/types/utilities.d.ts +7 -0
  151. package/dist/types/utilities.js +5 -0
  152. package/dist/types/utilities.js.map +1 -0
  153. package/package.json +10 -6
  154. package/src/__tests__/benchmark/benchmark.test.ts +1 -1
  155. package/src/__tests__/context.test.ts +91 -0
  156. package/src/__tests__/errors.test.ts +8 -5
  157. package/src/__tests__/globalEvents.test.ts +1 -1
  158. package/src/__tests__/globals/cache.middleware.test.ts +772 -0
  159. package/src/__tests__/globals/queue.resource.test.ts +141 -0
  160. package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
  161. package/src/__tests__/globals/retry.middleware.test.ts +157 -0
  162. package/src/__tests__/index.helper.test.ts +55 -0
  163. package/src/__tests__/models/EventManager.test.ts +144 -0
  164. package/src/__tests__/models/Logger.test.ts +291 -34
  165. package/src/__tests__/models/Queue.test.ts +189 -0
  166. package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
  167. package/src/__tests__/models/Semaphore.test.ts +713 -0
  168. package/src/__tests__/models/Store.test.ts +40 -0
  169. package/src/__tests__/models/TaskRunner.test.ts +86 -5
  170. package/src/__tests__/run.middleware.test.ts +166 -12
  171. package/src/__tests__/run.overrides.test.ts +13 -10
  172. package/src/__tests__/run.test.ts +363 -12
  173. package/src/__tests__/setOutput.test.ts +244 -0
  174. package/src/__tests__/tools/getCallerFile.test.ts +9 -9
  175. package/src/__tests__/typesafety.test.ts +54 -39
  176. package/src/context.ts +86 -0
  177. package/src/define.ts +84 -14
  178. package/src/defs.ts +91 -41
  179. package/src/errors.ts +3 -1
  180. package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
  181. package/src/globals/globalMiddleware.ts +14 -0
  182. package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
  183. package/src/globals/middleware/cache.middleware.ts +115 -0
  184. package/src/globals/middleware/requireContext.middleware.ts +36 -0
  185. package/src/globals/middleware/retry.middleware.ts +56 -0
  186. package/src/globals/resources/queue.resource.ts +34 -0
  187. package/src/index.ts +9 -5
  188. package/src/models/DependencyProcessor.ts +36 -40
  189. package/src/models/EventManager.ts +45 -5
  190. package/src/models/Logger.ts +170 -48
  191. package/src/models/OverrideManager.ts +84 -0
  192. package/src/models/Queue.ts +66 -0
  193. package/src/models/ResourceInitializer.ts +38 -20
  194. package/src/models/Semaphore.ts +208 -0
  195. package/src/models/Store.ts +94 -342
  196. package/src/models/StoreConstants.ts +17 -0
  197. package/src/models/StoreRegistry.ts +217 -0
  198. package/src/models/StoreTypes.ts +46 -0
  199. package/src/models/StoreValidator.ts +38 -0
  200. package/src/models/TaskRunner.ts +53 -40
  201. package/src/models/index.ts +3 -0
  202. package/src/run.ts +7 -4
  203. package/src/__tests__/index.ts +0 -15
  204. package/src/examples/express-mongo/index.ts +0 -1
@@ -0,0 +1,713 @@
1
+ import { Semaphore } from "../..";
2
+
3
+ describe("Semaphore", () => {
4
+ let semaphore: Semaphore;
5
+
6
+ beforeEach(() => {
7
+ semaphore = new Semaphore(2);
8
+ });
9
+
10
+ afterEach(() => {
11
+ if (!semaphore.isDisposed()) {
12
+ semaphore.dispose();
13
+ }
14
+ });
15
+
16
+ describe("constructor", () => {
17
+ it("should create semaphore with valid maxPermits", () => {
18
+ const sem = new Semaphore(5);
19
+ expect(sem.getMaxPermits()).toBe(5);
20
+ expect(sem.getAvailablePermits()).toBe(5);
21
+ expect(sem.getWaitingCount()).toBe(0);
22
+ expect(sem.isDisposed()).toBe(false);
23
+ sem.dispose();
24
+ });
25
+
26
+ it("should throw error for invalid maxPermits", () => {
27
+ expect(() => new Semaphore(0)).toThrow(
28
+ "maxPermits must be greater than 0"
29
+ );
30
+ expect(() => new Semaphore(-1)).toThrow(
31
+ "maxPermits must be greater than 0"
32
+ );
33
+ });
34
+ });
35
+
36
+ describe("acquire and release", () => {
37
+ it("should acquire permits when available", async () => {
38
+ await semaphore.acquire();
39
+ expect(semaphore.getAvailablePermits()).toBe(1);
40
+
41
+ await semaphore.acquire();
42
+ expect(semaphore.getAvailablePermits()).toBe(0);
43
+ });
44
+
45
+ it("should release permits correctly", async () => {
46
+ await semaphore.acquire();
47
+ await semaphore.acquire();
48
+ expect(semaphore.getAvailablePermits()).toBe(0);
49
+
50
+ semaphore.release();
51
+ expect(semaphore.getAvailablePermits()).toBe(1);
52
+
53
+ semaphore.release();
54
+ expect(semaphore.getAvailablePermits()).toBe(2);
55
+ });
56
+
57
+ it("should not exceed max permits on release", () => {
58
+ // Release without acquire should not exceed max
59
+ semaphore.release();
60
+ expect(semaphore.getAvailablePermits()).toBe(2); // Should stay at max
61
+ });
62
+
63
+ it("should queue operations when no permits available", async () => {
64
+ // Fill up all permits
65
+ await semaphore.acquire();
66
+ await semaphore.acquire();
67
+ expect(semaphore.getAvailablePermits()).toBe(0);
68
+
69
+ // Start a third operation that should wait
70
+ const pendingOperation = semaphore.acquire();
71
+ expect(semaphore.getWaitingCount()).toBe(1);
72
+
73
+ // Release a permit - should resolve the waiting operation
74
+ semaphore.release();
75
+ await pendingOperation;
76
+ expect(semaphore.getWaitingCount()).toBe(0);
77
+ expect(semaphore.getAvailablePermits()).toBe(0); // Permit went directly to waiting operation
78
+ });
79
+
80
+ it("should handle multiple waiting operations in FIFO order", async () => {
81
+ // Fill all permits
82
+ await semaphore.acquire();
83
+ await semaphore.acquire();
84
+
85
+ const results: number[] = [];
86
+
87
+ // Queue multiple operations
88
+ const op1 = semaphore.acquire().then(() => results.push(1));
89
+ const op2 = semaphore.acquire().then(() => results.push(2));
90
+ const op3 = semaphore.acquire().then(() => results.push(3));
91
+
92
+ expect(semaphore.getWaitingCount()).toBe(3);
93
+
94
+ // Release permits one by one
95
+ semaphore.release();
96
+ await op1;
97
+ expect(results).toEqual([1]);
98
+
99
+ semaphore.release();
100
+ await op2;
101
+ expect(results).toEqual([1, 2]);
102
+
103
+ semaphore.release();
104
+ await op3;
105
+ expect(results).toEqual([1, 2, 3]);
106
+ });
107
+ });
108
+
109
+ describe("withPermit", () => {
110
+ it("should execute function with permit and auto-release", async () => {
111
+ let executed = false;
112
+ const result = await semaphore.withPermit(async () => {
113
+ executed = true;
114
+ expect(semaphore.getAvailablePermits()).toBe(1); // One permit taken
115
+ return "success";
116
+ });
117
+
118
+ expect(executed).toBe(true);
119
+ expect(result).toBe("success");
120
+ expect(semaphore.getAvailablePermits()).toBe(2); // Permit released
121
+ });
122
+
123
+ it("should release permit even if function throws", async () => {
124
+ expect.assertions(2);
125
+
126
+ try {
127
+ await semaphore.withPermit(async () => {
128
+ expect(semaphore.getAvailablePermits()).toBe(1);
129
+ throw new Error("Test error");
130
+ });
131
+ } catch (error) {
132
+ expect(semaphore.getAvailablePermits()).toBe(2); // Permit still released
133
+ }
134
+ });
135
+
136
+ it("should queue withPermit operations when no permits available", async () => {
137
+ // Fill all permits
138
+ await semaphore.acquire();
139
+ await semaphore.acquire();
140
+
141
+ const results: string[] = [];
142
+
143
+ // Queue operations
144
+ const op1 = semaphore.withPermit(async () => {
145
+ results.push("op1");
146
+ return "result1";
147
+ });
148
+
149
+ const op2 = semaphore.withPermit(async () => {
150
+ results.push("op2");
151
+ return "result2";
152
+ });
153
+
154
+ expect(semaphore.getWaitingCount()).toBe(2);
155
+
156
+ // Release permits
157
+ semaphore.release();
158
+ await op1;
159
+ expect(results).toEqual(["op1"]);
160
+
161
+ semaphore.release();
162
+ await op2;
163
+ expect(results).toEqual(["op1", "op2"]);
164
+ });
165
+ });
166
+
167
+ describe("timeout support", () => {
168
+ it("should timeout acquire operation", async () => {
169
+ // Fill all permits
170
+ await semaphore.acquire();
171
+ await semaphore.acquire();
172
+
173
+ const startTime = Date.now();
174
+
175
+ await expect(semaphore.acquire({ timeout: 100 })).rejects.toThrow(
176
+ "Semaphore acquire timeout after 100ms"
177
+ );
178
+
179
+ const elapsed = Date.now() - startTime;
180
+ expect(elapsed).toBeGreaterThanOrEqual(100);
181
+ expect(elapsed).toBeLessThan(200); // Should not wait much longer
182
+ });
183
+
184
+ it("should timeout withPermit operation", async () => {
185
+ // Fill all permits
186
+ await semaphore.acquire();
187
+ await semaphore.acquire();
188
+
189
+ await expect(
190
+ semaphore.withPermit(async () => "never executed", { timeout: 100 })
191
+ ).rejects.toThrow("Semaphore acquire timeout after 100ms");
192
+ });
193
+
194
+ it("should clear timeout when operation succeeds", async () => {
195
+ // Fill all permits
196
+ await semaphore.acquire();
197
+ await semaphore.acquire();
198
+
199
+ // Start operation with timeout
200
+ const operationPromise = semaphore.acquire({ timeout: 1000 });
201
+
202
+ // Release permit quickly - should not timeout
203
+ setTimeout(() => semaphore.release(), 50);
204
+
205
+ await expect(operationPromise).resolves.toBeUndefined();
206
+ });
207
+
208
+ it("should handle zero or negative timeout", async () => {
209
+ // Fill all permits
210
+ await semaphore.acquire();
211
+ await semaphore.acquire();
212
+
213
+ // Zero timeout should be ignored
214
+ const operationPromise = semaphore.acquire({ timeout: 0 });
215
+ semaphore.release();
216
+ await expect(operationPromise).resolves.toBeUndefined();
217
+ });
218
+
219
+ it("should remove timed out operations from queue", async () => {
220
+ // Fill all permits
221
+ await semaphore.acquire();
222
+ await semaphore.acquire();
223
+
224
+ // Start operation that will timeout
225
+ const timeoutPromise = semaphore.acquire({ timeout: 50 });
226
+ expect(semaphore.getWaitingCount()).toBe(1);
227
+
228
+ await expect(timeoutPromise).rejects.toThrow("timeout");
229
+ expect(semaphore.getWaitingCount()).toBe(0);
230
+ });
231
+ });
232
+
233
+ describe("cancellation support", () => {
234
+ it("should cancel acquire operation with AbortSignal", async () => {
235
+ // Fill all permits
236
+ await semaphore.acquire();
237
+ await semaphore.acquire();
238
+
239
+ const controller = new AbortController();
240
+ const operationPromise = semaphore.acquire({ signal: controller.signal });
241
+
242
+ expect(semaphore.getWaitingCount()).toBe(1);
243
+
244
+ // Cancel the operation
245
+ controller.abort();
246
+
247
+ await expect(operationPromise).rejects.toThrow("Operation was aborted");
248
+ expect(semaphore.getWaitingCount()).toBe(0);
249
+ });
250
+
251
+ it("should cancel withPermit operation with AbortSignal", async () => {
252
+ // Fill all permits
253
+ await semaphore.acquire();
254
+ await semaphore.acquire();
255
+
256
+ const controller = new AbortController();
257
+ const operationPromise = semaphore.withPermit(
258
+ async () => "never executed",
259
+ { signal: controller.signal }
260
+ );
261
+
262
+ controller.abort();
263
+
264
+ await expect(operationPromise).rejects.toThrow("Operation was aborted");
265
+ });
266
+
267
+ it("should reject immediately if signal is already aborted", async () => {
268
+ const controller = new AbortController();
269
+ controller.abort();
270
+
271
+ await expect(
272
+ semaphore.acquire({ signal: controller.signal })
273
+ ).rejects.toThrow("Operation was aborted");
274
+ });
275
+
276
+ it("should clean up abort listeners when operation completes normally", async () => {
277
+ const controller = new AbortController();
278
+
279
+ // This should complete normally and clean up listeners
280
+ await semaphore.acquire({ signal: controller.signal });
281
+ semaphore.release();
282
+
283
+ // No way to directly test listener cleanup, but this ensures no memory leaks
284
+ expect(semaphore.getAvailablePermits()).toBe(2);
285
+ });
286
+
287
+ it("should clean up abort listeners when queued operation resolves", async () => {
288
+ // Fill all permits
289
+ await semaphore.acquire();
290
+ await semaphore.acquire();
291
+
292
+ const controller = new AbortController();
293
+
294
+ // Start operation that will wait in queue
295
+ const queuedOperation = semaphore.acquire({ signal: controller.signal });
296
+ expect(semaphore.getWaitingCount()).toBe(1);
297
+
298
+ // Release permit - should trigger the resolve path with cleanup
299
+ semaphore.release();
300
+ await queuedOperation;
301
+
302
+ expect(semaphore.getWaitingCount()).toBe(0);
303
+ expect(semaphore.getAvailablePermits()).toBe(0);
304
+ });
305
+
306
+ it("should clean up abort listeners when queued operation rejects due to timeout", async () => {
307
+ // Fill all permits
308
+ await semaphore.acquire();
309
+ await semaphore.acquire();
310
+
311
+ const controller = new AbortController();
312
+
313
+ // Start operation with both timeout and abort signal
314
+ const queuedOperation = semaphore.acquire({
315
+ timeout: 50,
316
+ signal: controller.signal,
317
+ });
318
+ expect(semaphore.getWaitingCount()).toBe(1);
319
+
320
+ // Let timeout occur - should trigger reject path with cleanup
321
+ await expect(queuedOperation).rejects.toThrow("timeout");
322
+
323
+ expect(semaphore.getWaitingCount()).toBe(0);
324
+ });
325
+
326
+ it("should handle both timeout and cancellation", async () => {
327
+ // Fill all permits
328
+ await semaphore.acquire();
329
+ await semaphore.acquire();
330
+
331
+ const controller = new AbortController();
332
+ const operationPromise = semaphore.acquire({
333
+ timeout: 1000,
334
+ signal: controller.signal,
335
+ });
336
+
337
+ // Cancel before timeout
338
+ setTimeout(() => controller.abort(), 50);
339
+
340
+ await expect(operationPromise).rejects.toThrow("Operation was aborted");
341
+ });
342
+ });
343
+
344
+ describe("dispose", () => {
345
+ it("should dispose semaphore and reject waiting operations", async () => {
346
+ // Fill all permits
347
+ await semaphore.acquire();
348
+ await semaphore.acquire();
349
+
350
+ // Queue some operations
351
+ const op1 = semaphore.acquire();
352
+ const op2 = semaphore.acquire();
353
+ const op3 = semaphore.withPermit(async () => "never executed");
354
+
355
+ expect(semaphore.getWaitingCount()).toBe(3);
356
+
357
+ // Dispose
358
+ semaphore.dispose();
359
+
360
+ // All operations should be rejected
361
+ await expect(op1).rejects.toThrow("Semaphore has been disposed");
362
+ await expect(op2).rejects.toThrow("Semaphore has been disposed");
363
+ await expect(op3).rejects.toThrow("Semaphore has been disposed");
364
+
365
+ expect(semaphore.getWaitingCount()).toBe(0);
366
+ expect(semaphore.isDisposed()).toBe(true);
367
+ });
368
+
369
+ it("should prevent new operations after disposal", async () => {
370
+ semaphore.dispose();
371
+
372
+ await expect(semaphore.acquire()).rejects.toThrow(
373
+ "Semaphore has been disposed"
374
+ );
375
+ await expect(
376
+ semaphore.withPermit(async () => "never executed")
377
+ ).rejects.toThrow("Semaphore has been disposed");
378
+ });
379
+
380
+ it("should ignore release after disposal", () => {
381
+ semaphore.dispose();
382
+
383
+ // Should not throw
384
+ semaphore.release();
385
+ expect(semaphore.isDisposed()).toBe(true);
386
+ });
387
+
388
+ it("should be idempotent", () => {
389
+ semaphore.dispose();
390
+ semaphore.dispose();
391
+ semaphore.dispose();
392
+
393
+ expect(semaphore.isDisposed()).toBe(true);
394
+ });
395
+
396
+ it("should clear timeouts when disposing", async () => {
397
+ // Fill all permits
398
+ await semaphore.acquire();
399
+ await semaphore.acquire();
400
+
401
+ // Start operation with timeout
402
+ const operationPromise = semaphore.acquire({ timeout: 1000 });
403
+
404
+ // Dispose immediately - should not wait for timeout
405
+ semaphore.dispose();
406
+
407
+ await expect(operationPromise).rejects.toThrow(
408
+ "Semaphore has been disposed"
409
+ );
410
+ });
411
+
412
+ it("should clean up abort listeners when operation is disposed", async () => {
413
+ // Fill all permits
414
+ await semaphore.acquire();
415
+ await semaphore.acquire();
416
+
417
+ const controller = new AbortController();
418
+
419
+ // Start operation with abort signal that will wait in queue
420
+ const queuedOperation = semaphore.acquire({ signal: controller.signal });
421
+ expect(semaphore.getWaitingCount()).toBe(1);
422
+
423
+ // Dispose semaphore - should trigger reject path with abort listener cleanup
424
+ semaphore.dispose();
425
+
426
+ await expect(queuedOperation).rejects.toThrow(
427
+ "Semaphore has been disposed"
428
+ );
429
+
430
+ expect(semaphore.getWaitingCount()).toBe(0);
431
+ });
432
+ });
433
+
434
+ describe("metrics and debugging", () => {
435
+ it("should provide accurate metrics", () => {
436
+ const metrics = semaphore.getMetrics();
437
+
438
+ expect(metrics.availablePermits).toBe(2);
439
+ expect(metrics.waitingCount).toBe(0);
440
+ expect(metrics.maxPermits).toBe(2);
441
+ expect(metrics.utilization).toBe(0);
442
+ expect(metrics.disposed).toBe(false);
443
+ });
444
+
445
+ it("should update metrics as operations progress", async () => {
446
+ // Acquire one permit
447
+ await semaphore.acquire();
448
+
449
+ let metrics = semaphore.getMetrics();
450
+ expect(metrics.availablePermits).toBe(1);
451
+ expect(metrics.utilization).toBe(0.5);
452
+
453
+ // Acquire second permit
454
+ await semaphore.acquire();
455
+
456
+ metrics = semaphore.getMetrics();
457
+ expect(metrics.availablePermits).toBe(0);
458
+ expect(metrics.utilization).toBe(1);
459
+
460
+ // Queue an operation
461
+ const pendingOp = semaphore.acquire();
462
+
463
+ metrics = semaphore.getMetrics();
464
+ expect(metrics.waitingCount).toBe(1);
465
+
466
+ // Release and complete
467
+ semaphore.release();
468
+ await pendingOp;
469
+
470
+ metrics = semaphore.getMetrics();
471
+ expect(metrics.availablePermits).toBe(0);
472
+ expect(metrics.waitingCount).toBe(0);
473
+ expect(metrics.utilization).toBe(1);
474
+ });
475
+
476
+ it("should provide individual metric getters", async () => {
477
+ expect(semaphore.getAvailablePermits()).toBe(2);
478
+ expect(semaphore.getWaitingCount()).toBe(0);
479
+ expect(semaphore.getMaxPermits()).toBe(2);
480
+ expect(semaphore.isDisposed()).toBe(false);
481
+
482
+ await semaphore.acquire();
483
+ expect(semaphore.getAvailablePermits()).toBe(1);
484
+
485
+ // Queue an operation
486
+ await semaphore.acquire();
487
+ const pendingOp = semaphore.acquire();
488
+ expect(semaphore.getWaitingCount()).toBe(1);
489
+
490
+ semaphore.dispose();
491
+ expect(semaphore.isDisposed()).toBe(true);
492
+
493
+ await expect(pendingOp).rejects.toThrow();
494
+ });
495
+ });
496
+
497
+ describe("edge cases and error handling", () => {
498
+ it("should handle rapid acquire/release cycles", async () => {
499
+ const operations: any[] = [];
500
+
501
+ // Rapid fire operations
502
+ for (let i = 0; i < 100; i++) {
503
+ operations.push(
504
+ semaphore.withPermit(async () => {
505
+ // Simulate quick work
506
+ await new Promise((resolve) => setTimeout(resolve, 1));
507
+ return i;
508
+ })
509
+ );
510
+ }
511
+
512
+ const results = await Promise.all(operations);
513
+ expect(results).toHaveLength(100);
514
+ expect(semaphore.getAvailablePermits()).toBe(2);
515
+ expect(semaphore.getWaitingCount()).toBe(0);
516
+ });
517
+
518
+ it("should handle concurrent dispose and operations", async () => {
519
+ // Fill permits
520
+ await semaphore.acquire();
521
+ await semaphore.acquire();
522
+
523
+ // Start multiple operations
524
+ const operations = [
525
+ semaphore.acquire(),
526
+ semaphore.acquire(),
527
+ semaphore.withPermit(async () => "test"),
528
+ ];
529
+
530
+ // Dispose concurrently
531
+ setTimeout(() => semaphore.dispose(), 10);
532
+
533
+ // All should be rejected
534
+ for (const op of operations) {
535
+ await expect(op).rejects.toThrow("Semaphore has been disposed");
536
+ }
537
+ });
538
+
539
+ it("should handle mixed timeout and non-timeout operations", async () => {
540
+ // Fill permits
541
+ await semaphore.acquire();
542
+ await semaphore.acquire();
543
+
544
+ // Mix of operations
545
+ const op1 = semaphore.acquire(); // No timeout
546
+ const op2 = semaphore.acquire({ timeout: 100 }); // Will timeout
547
+ const op3 = semaphore.acquire(); // No timeout
548
+
549
+ expect(semaphore.getWaitingCount()).toBe(3);
550
+
551
+ // Wait for timeout
552
+ await expect(op2).rejects.toThrow("timeout");
553
+ expect(semaphore.getWaitingCount()).toBe(2);
554
+
555
+ // Release permits for remaining operations
556
+ semaphore.release();
557
+ await op1;
558
+ semaphore.release();
559
+ await op3;
560
+ });
561
+
562
+ it("should maintain consistency under stress", async () => {
563
+ const concurrentOps = 50;
564
+ const operations: any[] = [];
565
+
566
+ // Start many concurrent operations
567
+ for (let i = 0; i < concurrentOps; i++) {
568
+ operations.push(
569
+ semaphore.withPermit(async () => {
570
+ // Random delay to create timing variations
571
+ await new Promise((resolve) =>
572
+ setTimeout(resolve, Math.random() * 10)
573
+ );
574
+ return i;
575
+ })
576
+ );
577
+ }
578
+
579
+ const results = await Promise.all(operations);
580
+
581
+ // Verify all operations completed
582
+ expect(results).toHaveLength(concurrentOps);
583
+ expect(semaphore.getAvailablePermits()).toBe(2);
584
+ expect(semaphore.getWaitingCount()).toBe(0);
585
+ });
586
+ });
587
+
588
+ describe("real-world scenarios", () => {
589
+ it("should work as database connection pool limiter", async () => {
590
+ const dbSemaphore = new Semaphore(3);
591
+ const connectionPool = {
592
+ activeConnections: 0,
593
+ maxConnections: 3,
594
+ async query(sql: string) {
595
+ return dbSemaphore.withPermit(async () => {
596
+ this.activeConnections++;
597
+ expect(this.activeConnections).toBeLessThanOrEqual(
598
+ this.maxConnections
599
+ );
600
+
601
+ // Simulate query time
602
+ await new Promise((resolve) => setTimeout(resolve, 10));
603
+
604
+ this.activeConnections--;
605
+ return `Result for: ${sql}`;
606
+ });
607
+ },
608
+ };
609
+
610
+ // Fire many concurrent queries
611
+ const queries = Array.from({ length: 10 }, (_, i) =>
612
+ connectionPool.query(`SELECT * FROM users WHERE id = ${i}`)
613
+ );
614
+
615
+ const results = await Promise.all(queries);
616
+ expect(results).toHaveLength(10);
617
+ expect(connectionPool.activeConnections).toBe(0);
618
+
619
+ dbSemaphore.dispose();
620
+ });
621
+
622
+ it("should work as rate limiter for API calls", async () => {
623
+ const rateLimiter = new Semaphore(2);
624
+ let activeCalls = 0;
625
+
626
+ const apiClient = {
627
+ async fetchUser(id: number, signal?: AbortSignal) {
628
+ return rateLimiter.withPermit(
629
+ async () => {
630
+ activeCalls++;
631
+ expect(activeCalls).toBeLessThanOrEqual(2);
632
+
633
+ // Simulate API call
634
+ await new Promise((resolve) => setTimeout(resolve, 20));
635
+
636
+ activeCalls--;
637
+ return { id, name: `User ${id}` };
638
+ },
639
+ { signal }
640
+ );
641
+ },
642
+ };
643
+
644
+ // Test normal operation
645
+ const users = await Promise.all([
646
+ apiClient.fetchUser(1),
647
+ apiClient.fetchUser(2),
648
+ apiClient.fetchUser(3),
649
+ apiClient.fetchUser(4),
650
+ ]);
651
+
652
+ expect(users).toHaveLength(4);
653
+ expect(activeCalls).toBe(0);
654
+
655
+ // Test with cancellation - start long-running operations to fill semaphore
656
+ const longRunningOp1 = apiClient.fetchUser(10);
657
+ const longRunningOp2 = apiClient.fetchUser(11);
658
+
659
+ // Now semaphore should be full, so next operation will wait
660
+ const controller = new AbortController();
661
+ const cancelledCall = apiClient.fetchUser(5, controller.signal);
662
+ controller.abort();
663
+
664
+ await expect(cancelledCall).rejects.toThrow("Operation was aborted");
665
+
666
+ // Wait for the long-running operations to complete
667
+ await longRunningOp1;
668
+ await longRunningOp2;
669
+
670
+ rateLimiter.dispose();
671
+ });
672
+
673
+ it("should handle batch processing with progress tracking", async () => {
674
+ const batchSemaphore = new Semaphore(3);
675
+ const items = Array.from({ length: 20 }, (_, i) => ({
676
+ id: i,
677
+ data: `item-${i}`,
678
+ }));
679
+ const processed: any[] = [];
680
+
681
+ const processBatch = async () => {
682
+ const promises = items.map((item) =>
683
+ batchSemaphore.withPermit(async () => {
684
+ // Simulate processing time
685
+ await new Promise((resolve) => setTimeout(resolve, 5));
686
+
687
+ const result = { ...item, processed: true };
688
+ processed.push(result);
689
+
690
+ // Track progress
691
+ const metrics = batchSemaphore.getMetrics();
692
+ expect(
693
+ metrics.maxPermits - metrics.availablePermits
694
+ ).toBeLessThanOrEqual(3);
695
+
696
+ return result;
697
+ })
698
+ );
699
+
700
+ return Promise.all(promises);
701
+ };
702
+
703
+ const results = await processBatch();
704
+
705
+ expect(results).toHaveLength(20);
706
+ expect(processed).toHaveLength(20);
707
+ expect(batchSemaphore.getAvailablePermits()).toBe(3);
708
+ expect(batchSemaphore.getWaitingCount()).toBe(0);
709
+
710
+ batchSemaphore.dispose();
711
+ });
712
+ });
713
+ });