@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.
- package/README.md +1315 -942
- package/dist/common.types.d.ts +20 -0
- package/dist/common.types.js +4 -0
- package/dist/common.types.js.map +1 -0
- package/dist/context.d.ts +34 -0
- package/dist/context.js +58 -0
- package/dist/context.js.map +1 -0
- package/dist/define.d.ts +22 -3
- package/dist/define.js +52 -8
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +52 -31
- package/dist/defs.js +10 -2
- package/dist/defs.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/event.types.d.ts +18 -0
- package/dist/event.types.js +4 -0
- package/dist/event.types.js.map +1 -0
- package/dist/examples/registrator-example.d.ts +122 -0
- package/dist/examples/registrator-example.js +147 -0
- package/dist/examples/registrator-example.js.map +1 -0
- package/dist/globals/globalEvents.d.ts +41 -0
- package/dist/globals/globalEvents.js +94 -0
- package/dist/globals/globalEvents.js.map +1 -0
- package/dist/globals/globalMiddleware.d.ts +23 -0
- package/dist/globals/globalMiddleware.js +15 -0
- package/dist/globals/globalMiddleware.js.map +1 -0
- package/dist/globals/globalResources.d.ts +27 -0
- package/dist/globals/globalResources.js +47 -0
- package/dist/globals/globalResources.js.map +1 -0
- package/dist/globals/middleware/cache.middleware.d.ts +34 -0
- package/dist/globals/middleware/cache.middleware.js +85 -0
- package/dist/globals/middleware/cache.middleware.js.map +1 -0
- package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
- package/dist/globals/middleware/requireContext.middleware.js +25 -0
- package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
- package/dist/globals/middleware/retry.middleware.d.ts +20 -0
- package/dist/globals/middleware/retry.middleware.js +34 -0
- package/dist/globals/middleware/retry.middleware.js.map +1 -0
- package/dist/globals/resources/queue.resource.d.ts +7 -0
- package/dist/globals/resources/queue.resource.js +31 -0
- package/dist/globals/resources/queue.resource.js.map +1 -0
- package/dist/index.d.ts +45 -9
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/middleware.types.d.ts +40 -0
- package/dist/middleware.types.js +4 -0
- package/dist/middleware.types.js.map +1 -0
- package/dist/models/DependencyProcessor.d.ts +2 -1
- package/dist/models/DependencyProcessor.js +11 -13
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.d.ts +5 -0
- package/dist/models/EventManager.js +44 -2
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Logger.d.ts +30 -12
- package/dist/models/Logger.js +130 -42
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/OverrideManager.d.ts +13 -0
- package/dist/models/OverrideManager.js +70 -0
- package/dist/models/OverrideManager.js.map +1 -0
- package/dist/models/Queue.d.ts +25 -0
- package/dist/models/Queue.js +54 -0
- package/dist/models/Queue.js.map +1 -0
- package/dist/models/ResourceInitializer.d.ts +5 -2
- package/dist/models/ResourceInitializer.js +20 -14
- package/dist/models/ResourceInitializer.js.map +1 -1
- package/dist/models/Semaphore.d.ts +61 -0
- package/dist/models/Semaphore.js +166 -0
- package/dist/models/Semaphore.js.map +1 -0
- package/dist/models/Store.d.ts +17 -72
- package/dist/models/Store.js +71 -269
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreConstants.d.ts +11 -0
- package/dist/models/StoreConstants.js +18 -0
- package/dist/models/StoreConstants.js.map +1 -0
- package/dist/models/StoreRegistry.d.ts +25 -0
- package/dist/models/StoreRegistry.js +171 -0
- package/dist/models/StoreRegistry.js.map +1 -0
- package/dist/models/StoreTypes.d.ts +21 -0
- package/dist/models/StoreTypes.js +3 -0
- package/dist/models/StoreTypes.js.map +1 -0
- package/dist/models/StoreValidator.d.ts +10 -0
- package/dist/models/StoreValidator.js +41 -0
- package/dist/models/StoreValidator.js.map +1 -0
- package/dist/models/TaskRunner.js +39 -24
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/models/VarStore.d.ts +17 -0
- package/dist/models/VarStore.js +60 -0
- package/dist/models/VarStore.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/index.js.map +1 -1
- package/dist/resource.types.d.ts +31 -0
- package/dist/resource.types.js +3 -0
- package/dist/resource.types.js.map +1 -0
- package/dist/run.d.ts +4 -1
- package/dist/run.js +6 -3
- package/dist/run.js.map +1 -1
- package/dist/symbols.d.ts +24 -0
- package/dist/symbols.js +29 -0
- package/dist/symbols.js.map +1 -0
- package/dist/task.types.d.ts +55 -0
- package/dist/task.types.js +23 -0
- package/dist/task.types.js.map +1 -0
- package/dist/tools/registratorId.d.ts +4 -0
- package/dist/tools/registratorId.js +40 -0
- package/dist/tools/registratorId.js.map +1 -0
- package/dist/tools/simpleHash.d.ts +9 -0
- package/dist/tools/simpleHash.js +34 -0
- package/dist/tools/simpleHash.js.map +1 -0
- package/dist/types/base-interfaces.d.ts +18 -0
- package/dist/types/base-interfaces.js +6 -0
- package/dist/types/base-interfaces.js.map +1 -0
- package/dist/types/base.d.ts +13 -0
- package/dist/types/base.js +3 -0
- package/dist/types/base.js.map +1 -0
- package/dist/types/dependencies.d.ts +22 -0
- package/dist/types/dependencies.js +3 -0
- package/dist/types/dependencies.js.map +1 -0
- package/dist/types/dependency-core.d.ts +14 -0
- package/dist/types/dependency-core.js +5 -0
- package/dist/types/dependency-core.js.map +1 -0
- package/dist/types/events.d.ts +52 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/hooks.d.ts +16 -0
- package/dist/types/hooks.js +5 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.js +27 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/meta.d.ts +13 -0
- package/dist/types/meta.js +5 -0
- package/dist/types/meta.js.map +1 -0
- package/dist/types/middleware.d.ts +38 -0
- package/dist/types/middleware.js +6 -0
- package/dist/types/middleware.js.map +1 -0
- package/dist/types/registerable.d.ts +10 -0
- package/dist/types/registerable.js +5 -0
- package/dist/types/registerable.js.map +1 -0
- package/dist/types/resources.d.ts +44 -0
- package/dist/types/resources.js +5 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/symbols.d.ts +24 -0
- package/dist/types/symbols.js +30 -0
- package/dist/types/symbols.js.map +1 -0
- package/dist/types/tasks.d.ts +41 -0
- package/dist/types/tasks.js +5 -0
- package/dist/types/tasks.js.map +1 -0
- package/dist/types/utilities.d.ts +7 -0
- package/dist/types/utilities.js +5 -0
- package/dist/types/utilities.js.map +1 -0
- package/package.json +10 -6
- package/src/__tests__/benchmark/benchmark.test.ts +1 -1
- package/src/__tests__/context.test.ts +91 -0
- package/src/__tests__/errors.test.ts +8 -5
- package/src/__tests__/globalEvents.test.ts +1 -1
- package/src/__tests__/globals/cache.middleware.test.ts +772 -0
- package/src/__tests__/globals/queue.resource.test.ts +141 -0
- package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
- package/src/__tests__/globals/retry.middleware.test.ts +157 -0
- package/src/__tests__/index.helper.test.ts +55 -0
- package/src/__tests__/models/EventManager.test.ts +144 -0
- package/src/__tests__/models/Logger.test.ts +291 -34
- package/src/__tests__/models/Queue.test.ts +189 -0
- package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
- package/src/__tests__/models/Semaphore.test.ts +713 -0
- package/src/__tests__/models/Store.test.ts +40 -0
- package/src/__tests__/models/TaskRunner.test.ts +86 -5
- package/src/__tests__/run.middleware.test.ts +166 -12
- package/src/__tests__/run.overrides.test.ts +13 -10
- package/src/__tests__/run.test.ts +363 -12
- package/src/__tests__/setOutput.test.ts +244 -0
- package/src/__tests__/tools/getCallerFile.test.ts +9 -9
- package/src/__tests__/typesafety.test.ts +54 -39
- package/src/context.ts +86 -0
- package/src/define.ts +84 -14
- package/src/defs.ts +91 -41
- package/src/errors.ts +3 -1
- package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
- package/src/globals/globalMiddleware.ts +14 -0
- package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
- package/src/globals/middleware/cache.middleware.ts +115 -0
- package/src/globals/middleware/requireContext.middleware.ts +36 -0
- package/src/globals/middleware/retry.middleware.ts +56 -0
- package/src/globals/resources/queue.resource.ts +34 -0
- package/src/index.ts +9 -5
- package/src/models/DependencyProcessor.ts +36 -40
- package/src/models/EventManager.ts +45 -5
- package/src/models/Logger.ts +170 -48
- package/src/models/OverrideManager.ts +84 -0
- package/src/models/Queue.ts +66 -0
- package/src/models/ResourceInitializer.ts +38 -20
- package/src/models/Semaphore.ts +208 -0
- package/src/models/Store.ts +94 -342
- package/src/models/StoreConstants.ts +17 -0
- package/src/models/StoreRegistry.ts +217 -0
- package/src/models/StoreTypes.ts +46 -0
- package/src/models/StoreValidator.ts +38 -0
- package/src/models/TaskRunner.ts +53 -40
- package/src/models/index.ts +3 -0
- package/src/run.ts +7 -4
- package/src/__tests__/index.ts +0 -15
- 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
|
+
});
|