zero-backpressure-semaphore-typescript
Version:
A modern Promise-semaphore for Node.js projects, enabling users to limit the number of concurrently executing promises. Offering backpressure control for enhanced efficiency, utilizing a communicative API that signals availability, promoting a just-in-tim
493 lines • 29.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const zero_backpressure_semaphore_1 = require("./zero-backpressure-semaphore");
/**
* resolveFast
*
* The one-and-only purpose of this function, is triggerring an event-loop iteration.
* It is relevant whenever a test needs to simulate tasks from the Node.js' micro-tasks queue.
*/
const resolveFast = async () => {
expect(14).toBeGreaterThan(3);
};
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
describe('ZeroBackpressureSemaphore tests', () => {
describe('Happy path tests', () => {
// prettier-ignore
test('waitForCompletion: should process only one job at a time, ' +
'when jobs happen to be scheduled sequentially (trivial case)', async () => {
const maxConcurrentJobs = 7;
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
let completeCurrentJob;
const numberOfJobs = 10;
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
expect(semaphore.isAvailable).toBeTruthy();
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
expect(semaphore.maxConcurrentJobs).toBe(maxConcurrentJobs);
const jobPromise = new Promise((res) => (completeCurrentJob = res));
const job = () => jobPromise;
const waitForCompletionPromise = semaphore.waitForCompletion(job);
await Promise.race([waitForCompletionPromise, resolveFast()]);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(1);
// Trigger the completion of the current job.
completeCurrentJob();
await waitForCompletionPromise;
}
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
expect(semaphore.amountOfUncaughtErrors).toBe(0);
});
// prettier-ignore
test('waitForCompletion: should process only one job at a time, ' +
'when max concurrency is 1 and jobs are scheduled concurrently', async () => {
const maxConcurrentJobs = 1;
const lock = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
const numberOfJobs = 10;
const jobCompletionCallbacks = [];
const waitForCompletionPromises = [];
// Create a burst of jobs, inducing backpressure on the semaphore.
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
const jobPromise = new Promise((res) => (jobCompletionCallbacks[ithJob] = res));
const job = () => jobPromise;
// Jobs will be executed in the order in which they were registered.
waitForCompletionPromises.push(lock.waitForCompletion(job));
// Trigger the event loop to allow the semaphore to evaluate if the current job
// can begin execution.
// Based on this test's configuration, only the first job will be allowed to start.
await Promise.race([
waitForCompletionPromises[waitForCompletionPromises.length - 1],
resolveFast(),
]);
}
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
// At this stage, all jobs are pending for execution, except one which has started.
// At this stage, the ithJob has started its execution.
expect(lock.isAvailable).toBe(false);
expect(lock.amountOfCurrentlyExecutingJobs).toBe(1);
expect(lock.maxConcurrentJobs).toBe(maxConcurrentJobs);
// Complete the current job.
// Note: the order in which jobs start execution corresponds to the order in which
// `waitForCompletion` was invoked.
const finishCurrentJob = jobCompletionCallbacks[ithJob];
expect(finishCurrentJob).toBeDefined();
finishCurrentJob();
await waitForCompletionPromises[ithJob];
}
expect(lock.isAvailable).toBe(true);
expect(lock.amountOfCurrentlyExecutingJobs).toBe(0);
expect(lock.maxConcurrentJobs).toBe(maxConcurrentJobs);
expect(lock.amountOfUncaughtErrors).toBe(0);
});
// prettier-ignore
test('waitForCompletion: should not exceed max concurrently executing jobs, ' +
'when the amont of pending jobs is bigger than the amount of slots', async () => {
const maxConcurrentJobs = 5;
const numberOfJobs = 17 * maxConcurrentJobs - 1;
const jobCompletionCallbacks = [];
const waitForCompletionPromises = [];
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
// Create a burst of jobs, inducing backpressure on the semaphore.
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
const jobPromise = new Promise((res) => (jobCompletionCallbacks[ithJob] = res));
const job = () => jobPromise;
// Jobs will be executed in the order in which they were registered.
waitForCompletionPromises.push(semaphore.waitForCompletion(job));
// Triggering the event loop, allowing the semaphore to decide which jobs can
// start their execution. Based on this test's configuration, only the first
// `maxConcurrentJobs` jobs will be allowed to start.
await Promise.race([
waitForCompletionPromises[waitForCompletionPromises.length - 1],
resolveFast(),
]);
}
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
// At this stage, jobs [ithJob, min(maxConcurrentJobs, ithJob + maxConcurrentJobs - 1)]
// are executing.
const remainedJobs = numberOfJobs - ithJob;
const isAvailable = remainedJobs < maxConcurrentJobs;
const amountOfCurrentlyExecutingJobs = isAvailable ? remainedJobs : maxConcurrentJobs;
expect(semaphore.isAvailable).toBe(isAvailable);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(amountOfCurrentlyExecutingJobs);
expect(semaphore.maxConcurrentJobs).toBe(maxConcurrentJobs);
// Complete the current job.
// Note: the order in which jobs start execution corresponds to the order in which
// `waitForCompletion` was invoked.
const finishCurrentJob = jobCompletionCallbacks[ithJob];
expect(finishCurrentJob).toBeDefined();
finishCurrentJob();
await waitForCompletionPromises[ithJob];
}
expect(semaphore.isAvailable).toBe(true);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
expect(semaphore.maxConcurrentJobs).toBe(maxConcurrentJobs);
expect(semaphore.amountOfUncaughtErrors).toBe(0);
});
test('waitForCompletion: should return the expected value when succeeds', async () => {
const maxConcurrentJobs = 18;
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
const expectedReturnValue = -1723598;
const job = () => Promise.resolve(expectedReturnValue);
const actualReturnValue = await semaphore.waitForCompletion(job);
expect(actualReturnValue).toBe(expectedReturnValue);
expect(semaphore.amountOfUncaughtErrors).toBe(0);
});
test('waitForCompletion: should return the expected error when throws', async () => {
const maxConcurrentJobs = 3;
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
const expectedThrownError = new Error('mock error');
const job = () => Promise.reject(expectedThrownError);
try {
await semaphore.waitForCompletion(job);
expect(true).toBe(false); // The flow should not reach this point.
}
catch (actualThrownError) {
expect(actualThrownError).toBe(expectedThrownError);
expect(actualThrownError.message).toEqual(expectedThrownError.message);
}
// The semaphore stores uncaught errors only for background jobs triggered by
// `startExecution`.
expect(semaphore.amountOfUncaughtErrors).toBe(0);
});
// prettier-ignore
test('waitForAllExecutingJobsToComplete should resolve once all executing jobs have completed: ' +
'Jobs are resolved in FIFO order in this test', async () => {
const maxConcurrentJobs = 56;
const jobCompletionCallbacks = [];
const waitForCompletionPromises = [];
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
for (let ithJob = 0; ithJob < maxConcurrentJobs; ++ithJob) {
const jobPromise = new Promise((res) => (jobCompletionCallbacks[ithJob] = res));
const job = () => jobPromise;
// Jobs will be executed in the order in which they were registered.
waitForCompletionPromises.push(semaphore.waitForCompletion(job));
// Trigger the event loop.
await Promise.race([
waitForCompletionPromises[waitForCompletionPromises.length - 1],
resolveFast(),
]);
}
// At this point, the semaphore should be fully utilized.
expect(semaphore.isAvailable).toBe(false);
let allJobsCompleted = false;
const waitForAllExecutingJobsToCompletePromise = (async () => {
await semaphore.waitForAllExecutingJobsToComplete();
allJobsCompleted = true;
})();
// Trigger the event loop to verify that allJobsCompleted remains false.
await Promise.race([waitForAllExecutingJobsToCompletePromise, resolveFast()]);
// Resolve jobs one by one.
for (let ithJob = 0; ithJob < maxConcurrentJobs; ++ithJob) {
// Before resolving.
expect(allJobsCompleted).toBe(false);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(maxConcurrentJobs - ithJob);
// Resolve one job.
jobCompletionCallbacks[ithJob]();
await Promise.race([
waitForAllExecutingJobsToCompletePromise,
waitForCompletionPromises[ithJob], // Always wins the race, except potentially in the last iteration.
]);
// After resolving.
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(maxConcurrentJobs - ithJob - 1);
expect(semaphore.isAvailable).toBe(true);
}
await waitForAllExecutingJobsToCompletePromise;
expect(allJobsCompleted).toBe(true);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
expect(semaphore.amountOfUncaughtErrors).toBe(0);
});
// prettier-ignore
test('waitForAllExecutingJobsToComplete should resolve once all executing jobs have completed: ' +
'Jobs are resolved in FILO order in this test', async () => {
// FILO order for job completion times is less likely in real life, but it’s a good
// edge case to test.
// It ensures the semaphore can maintain a reference to an old job, even if its execution
// time exceeds all others.
const maxConcurrentJobs = 47;
const jobCompletionCallbacks = [];
const waitForCompletionPromises = [];
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
for (let ithJob = 0; ithJob < maxConcurrentJobs; ++ithJob) {
const jobPromise = new Promise((res) => (jobCompletionCallbacks[ithJob] = res));
const job = () => jobPromise;
// Jobs will be executed in the order in which they were registered.
waitForCompletionPromises.push(semaphore.waitForCompletion(job));
// Trigger the event loop.
await Promise.race([
waitForCompletionPromises[waitForCompletionPromises.length - 1],
resolveFast(),
]);
}
// At this point, the semaphore should be fully utilized.
expect(semaphore.isAvailable).toBe(false);
let allJobsCompleted = false;
const waitForAllExecutingJobsToCompletePromise = (async () => {
await semaphore.waitForAllExecutingJobsToComplete();
allJobsCompleted = true;
})();
// Trigger the event loop to verify that allJobsCompleted remains false.
await Promise.race([waitForAllExecutingJobsToCompletePromise, resolveFast()]);
// Resolve jobs one by one.
let expectedAmountOfCurrentlyExecutingJobs = maxConcurrentJobs;
for (let ithJob = maxConcurrentJobs - 1; ithJob >= 0; --ithJob) {
// Before resolving.
expect(allJobsCompleted).toBe(false);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(expectedAmountOfCurrentlyExecutingJobs);
// Resolve one job.
jobCompletionCallbacks.pop()();
await Promise.race([
waitForAllExecutingJobsToCompletePromise,
waitForCompletionPromises.pop(), // Always wins the race, except potentially in the last iteration.
]);
--expectedAmountOfCurrentlyExecutingJobs;
// After resolving.
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(expectedAmountOfCurrentlyExecutingJobs);
expect(semaphore.isAvailable).toBe(true);
}
await waitForAllExecutingJobsToCompletePromise;
expect(allJobsCompleted).toBe(true);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
expect(semaphore.amountOfUncaughtErrors).toBe(0);
});
// prettier-ignore
test('waitForAllExecutingJobsToComplete with the considerPendingJobsBackpressure flag set should ' +
'resolve once all executing jobs and pending jobs (i.e., backpressure) have completed', async () => {
jest.useFakeTimers();
const maxConcurrentJobs = 24;
const considerPendingJobsBackpressure = true;
const fullConcurrencyCycles = 35;
const lastCycleConcurrency = 9;
const numberOfJobs = fullConcurrencyCycles * maxConcurrentJobs + lastCycleConcurrency;
const jobDurationMs = 4000;
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
let completedJobsCounter = 0;
const job = async () => {
await delay(jobDurationMs);
++completedJobsCounter;
};
for (let ithJob = 0; ithJob < maxConcurrentJobs; ++ithJob) {
// We do not await here, but in practice, the caller should await the result before
// proceeding. The `waitForCompletion` method is typically used by multiple independent
// callers.
semaphore.waitForCompletion(job);
}
// We intentionally create the `waitForAllExecutingJobsToComplete` promise before adding
// pending jobs, which cannot be executed immediately. By using the `considerPendingJobsBackpressure`
// flag, the method will account for existing or future backpressure, even if it arises *after*
// the method is invoked.
let allJobsCompleted = false;
const allJobsCompletedPromise = (async () => {
await semaphore.waitForAllExecutingJobsToComplete(considerPendingJobsBackpressure);
allJobsCompleted = true;
})();
// Induce backpressure of pending jobs.
for (let ithJob = maxConcurrentJobs; ithJob < numberOfJobs; ++ithJob) {
semaphore.waitForCompletion(job);
}
for (let ithCycle = 1; ithCycle <= fullConcurrencyCycles; ++ithCycle) {
// Ensure that `waitForAllExecutingJobsToComplete` does not resolve prematurely.
expect(allJobsCompleted).toBe(false);
await Promise.race([
jest.advanceTimersByTimeAsync(jobDurationMs), // The race winner.
allJobsCompletedPromise,
]);
expect(completedJobsCounter).toBe(ithCycle * maxConcurrentJobs);
}
// The final cycle does not use the full concurrency capacity.
expect(allJobsCompleted).toBe(false);
await jest.advanceTimersByTimeAsync(jobDurationMs);
await allJobsCompletedPromise;
expect(allJobsCompleted).toBe(true);
expect(completedJobsCounter).toBe(numberOfJobs);
jest.restoreAllMocks();
jest.useRealTimers();
});
test('startExecution: background jobs should not exceed the max given concurrency', async () => {
const maxConcurrentJobs = 5;
const numberOfJobs = 6 * maxConcurrentJobs - 1;
const jobCompletionCallbacks = [];
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
// Each main iteration starts execution of the current ithJob, and completes the
// (ithJob - maxConcurrentJobs)th job if it exists, to free up a slot for the newly added job.
// To validate complex scenarios, even-numbered jobs will succeed while odd-numbered jobs
// will throw exceptions. From the semaphore's perspective, a completed job should release
// its associated slot, regardless of whether it completed successfully or failed.
let numberOfFailedJobs = 0;
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
const shouldJobSucceed = ithJob % 2 === 0;
if (!shouldJobSucceed) {
++numberOfFailedJobs;
}
// prettier-ignore
const jobPromise = new Promise((res, rej) => {
jobCompletionCallbacks[ithJob] = shouldJobSucceed
? () => res()
: () => rej(new Error('Why bad things happen to good semaphores?'));
});
const job = () => jobPromise;
// Jobs will be executed in the order in which they were registered.
const waitUntilExecutionStartsPromise = semaphore.startExecution(job);
if (ithJob < maxConcurrentJobs) {
// Should start immediately.
await waitUntilExecutionStartsPromise;
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(ithJob + 1);
expect(semaphore.maxConcurrentJobs).toBe(maxConcurrentJobs);
continue;
}
// At this stage, jobs [ithJob - maxConcurrentJobs, jobNo - 1] are executing,
// while the ithJob cannot start yet (none of the currently executing ones has completed).
expect(semaphore.isAvailable).toBe(false);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(maxConcurrentJobs);
expect(semaphore.maxConcurrentJobs).toBe(maxConcurrentJobs);
// Complete the oldest job (the first to begin execution among the currently running jobs),
// to free up an execution slot.
const completeOldestJob = jobCompletionCallbacks[ithJob - maxConcurrentJobs];
expect(completeOldestJob).toBeDefined();
completeOldestJob();
// After ensuring there is an available slot for the current job, wait until
// it starts execution.
await waitUntilExecutionStartsPromise;
}
// Completing the remaining "tail" of still-executing jobs:
// Each iteration of the main loop completes the current job.
const remainedJobsSuffixStart = numberOfJobs - maxConcurrentJobs;
let expectedAmountOfCurrentlyExecutingJobs = maxConcurrentJobs;
for (let ithJob = remainedJobsSuffixStart; ithJob < numberOfJobs; ++ithJob) {
const completeCurrentJob = jobCompletionCallbacks[ithJob];
expect(completeCurrentJob).toBeDefined();
completeCurrentJob();
// Trigger the event loop.
await resolveFast();
--expectedAmountOfCurrentlyExecutingJobs;
expect(semaphore.isAvailable).toBe(true);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(expectedAmountOfCurrentlyExecutingJobs);
expect(semaphore.maxConcurrentJobs).toBe(maxConcurrentJobs);
}
expect(semaphore.isAvailable).toBe(true);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
expect(semaphore.amountOfUncaughtErrors).toBe(numberOfFailedJobs);
});
test('waitForAvailability: should resolve once at least one slot is available', async () => {
const maxConcurrentJobs = 11;
const jobCompletionCallbacks = [];
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
for (let ithJob = 0; ithJob < maxConcurrentJobs; ++ithJob) {
expect(semaphore.isAvailable).toBe(true);
const jobPromise = new Promise((res) => (jobCompletionCallbacks[ithJob] = res));
await semaphore.startExecution(() => jobPromise); // Should resolve immediately.
}
expect(semaphore.isAvailable).toBe(false);
let finishedWaitingForAvailability = false;
const waitForAvailabilityPromise = (async () => {
await semaphore.waitForAvailability();
finishedWaitingForAvailability = true;
})();
// Perform some event loop iterations, without resolving any ongoing semaphore job.
// We expect `waitForAvailabilityPromise` to not be resolved.
const numberOfEventLoopIterationsWithoutExpectedChange = 197;
for (let eventLoopIteration = 0; eventLoopIteration < numberOfEventLoopIterationsWithoutExpectedChange; ++eventLoopIteration) {
await Promise.race([waitForAvailabilityPromise, resolveFast()]);
expect(semaphore.isAvailable).toBe(false);
expect(finishedWaitingForAvailability).toBe(false);
}
// Resolve one random job.
const randomJobIndexToResolveFirst = Math.floor(Math.random() * maxConcurrentJobs);
jobCompletionCallbacks[randomJobIndexToResolveFirst]();
const deleteCount = 1;
jobCompletionCallbacks.splice(randomJobIndexToResolveFirst, deleteCount);
// Now, we expect the semaphore to become available.
await waitForAvailabilityPromise;
expect(semaphore.isAvailable).toBe(true);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(maxConcurrentJobs - 1);
expect(finishedWaitingForAvailability).toBe(true);
// Clean pending promises.
for (const jobCompletionCallback of jobCompletionCallbacks) {
jobCompletionCallback();
}
await semaphore.waitForAllExecutingJobsToComplete();
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
});
// prettier-ignore
test('when _waitForAvailableSlot resolves, its awaiters should be executed according to ' +
'their order in the microtasks queue', async () => {
// This test does not directly assess the semaphore component. Instead, it verifies the
// correctness of the slot-acquire mechanism, ensuring it honors the FIFO order of callers
// requesting an available slot.
// In JavaScript, it is common for a caller to create a promise (as the sole owner of
// this promise instance) and await its resolution. It is less common for multiple promises
// to await concurrently on the same shared promise instance. In that scenario, a pertinent
// question arises:
// In which *order* will the multiple awaiters be executed?
// Short answer: according to their order in the Node.js microtasks queue.
// Long answer:
// When a promise is resolved, the callbacks attached to it (other promises awaiting
// its resolution) are *queued* as microtasks. Therefore, if multiple awaiters are waiting on
// the same shared promise instance, and the awaiters were created in a *specific* order, the
// first awaiter will be executed first once the shared promise is resolved. This is because
// adding a microtask (such as an async function awaiting a promise) ensures its position in
// the microtasks queue, guaranteeing its execution before subsequent microtasks in the queue.
// This holds true for any position, i.e., it can be generalized.
// In the following test, a relatively large number of awaiters is chosen. The motive is
// to observe statistical errors, which should *not* exist regardless of the input size.
const numberOfAwaiters = 384;
const actualExecutionOrderOfAwaiters = [];
// This specific usage of one promise instance being awaited by multiple other promises
// may remind those with a C++ background of a condition_variable.
let notifyAvailableSlotExists;
const waitForAvailableSlot = new Promise((res) => (notifyAvailableSlotExists = res));
const awaiterAskingForSlot = async (awaiterID) => {
await waitForAvailableSlot;
actualExecutionOrderOfAwaiters.push(awaiterID);
// Other awaiters in the microtasks queue will now be notified about the
// fulfillment of 'waitForAvailableSlot'.
};
const expectedExecutionOrder = [];
const awaiterPromises = [];
for (let awaiterID = 0; awaiterID < numberOfAwaiters; ++awaiterID) {
expectedExecutionOrder.push(awaiterID);
awaiterPromises.push(awaiterAskingForSlot(awaiterID));
}
// Initially, no awaiter should be able to make progress.
await Promise.race([...awaiterPromises, resolveFast()]);
expect(actualExecutionOrderOfAwaiters.length).toBe(0);
// Notify that a slot is available, triggering the awaiters in order.
notifyAvailableSlotExists();
await Promise.all(awaiterPromises);
// The execution order should match the expected order.
expect(actualExecutionOrderOfAwaiters).toEqual(expectedExecutionOrder);
});
});
describe('Negative path tests', () => {
test('should throw when maxConcurrentJobs is non-positive', () => {
expect(() => new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(-5)).toThrow();
expect(() => new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(0)).toThrow();
});
test('should throw when maxConcurrentJobs is non-natural', () => {
expect(() => new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(0.01)).toThrow();
expect(() => new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(1.99)).toThrow();
expect(() => new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(17.41)).toThrow();
});
test('should capture uncaught errors from background jobs triggered by startExecution', async () => {
const maxConcurrentJobs = 17;
const numberOfJobs = maxConcurrentJobs + 18;
const jobErrors = [];
const semaphore = new zero_backpressure_semaphore_1.ZeroBackpressureSemaphore(maxConcurrentJobs);
for (let ithJob = 0; ithJob < numberOfJobs; ++ithJob) {
const error = {
name: 'CustomJobError',
message: `Job no. ${ithJob} has failed`,
jobID: ithJob,
};
jobErrors.push(error);
await semaphore.startExecution(async () => {
throw error;
});
}
await semaphore.waitForAllExecutingJobsToComplete();
expect(semaphore.amountOfUncaughtErrors).toBe(numberOfJobs);
expect(semaphore.extractUncaughtErrors()).toEqual(jobErrors);
// Following extraction, the semaphore no longer holds the error references.
expect(semaphore.amountOfUncaughtErrors).toBe(0);
expect(semaphore.amountOfCurrentlyExecutingJobs).toBe(0);
});
});
});
//# sourceMappingURL=zero-backpressure-semaphore.test.js.map