@serenity-js/core
Version:
The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure
154 lines • 4.78 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scheduler = void 0;
const errors_1 = require("../../../errors");
const Duration_1 = require("./Duration");
/**
* @group Time
*/
class Scheduler {
clock;
interactionTimeout;
scheduledOperations = [];
/**
* @param clock
* @param interactionTimeout
* The maximum amount of time to give to a callback to complete before throwing an error
*/
constructor(clock, interactionTimeout) {
this.clock = clock;
this.interactionTimeout = interactionTimeout;
}
toJSON() {
return {
clock: this.clock.toJSON(),
interactionTimeout: this.interactionTimeout.toJSON(),
};
}
/**
* Schedules a callback function to be invoked after a delay
*
* @param delay
* @param callback
*/
after(delay, callback) {
return this.repeatUntil(callback, {
maxInvocations: 1,
delayBetweenInvocations: () => delay,
timeout: this.interactionTimeout.plus(delay),
});
}
/**
* Returns a `Promise` to be resolved after a `delay`
*
* @param delay
*/
waitFor(delay) {
return this.repeatUntil(() => void 0, {
maxInvocations: 1,
delayBetweenInvocations: () => delay,
// make sure waitFor doesn't get terminated before it's resolved
timeout: this.interactionTimeout.plus(delay),
});
}
/**
* Schedules a callback function to be repeated, according to configured limits.
*
* @param callback
* @param limits
*/
async repeatUntil(callback, limits = {}) {
const { maxInvocations = Number.POSITIVE_INFINITY, delayBetweenInvocations = noDelay, timeout = this.interactionTimeout, exitCondition = noEarlyExit, errorHandler = rethrowErrors, } = limits;
const operation = new ScheduledOperation(this.clock, callback, {
exitCondition,
maxInvocations,
delayBetweenInvocations,
timeout,
errorHandler,
});
this.scheduledOperations.push(operation);
return operation.start();
}
stop() {
for (const operation of this.scheduledOperations) {
operation.cancel();
}
}
}
exports.Scheduler = Scheduler;
class ScheduledOperation {
clock;
callback;
limits;
currentInvocation = 0;
invocationsLeft = 0;
startedAt;
lastResult;
isCancelled = false;
constructor(clock, callback, limits = {}) {
this.clock = clock;
this.callback = callback;
this.limits = limits;
}
async start() {
this.currentInvocation = 0;
this.invocationsLeft = this.limits.maxInvocations;
this.startedAt = this.clock.now();
return await this.poll();
}
async poll() {
await this.clock.waitFor(this.limits.delayBetweenInvocations(this.currentInvocation));
if (this.isCancelled) {
throw new errors_1.OperationInterruptedError('Scheduler stopped before executing callback');
}
const receipt = await this.invoke();
if (receipt.hasCompleted) {
return receipt.result;
}
this.currentInvocation++;
this.invocationsLeft--;
return await this.poll();
}
async invoke() {
const timeoutExpired = this.startedAt.plus(this.limits.timeout).isBefore(this.clock.now());
const isLastInvocation = this.invocationsLeft === 1;
if (this.invocationsLeft === 0) {
return {
result: this.lastResult,
hasCompleted: true,
};
}
try {
if (timeoutExpired) {
throw new errors_1.TimeoutExpiredError(`Timeout of ${this.limits.timeout} has expired`);
}
this.lastResult = await this.callback({ currentTime: this.clock.now(), i: this.currentInvocation });
return {
result: this.lastResult,
hasCompleted: this.limits.exitCondition(this.lastResult) || isLastInvocation,
};
}
catch (error) {
this.limits.errorHandler(error, this.lastResult);
// if the errorHandler didn't throw, it's a recoverable error
return {
result: this.lastResult,
error,
hasCompleted: isLastInvocation,
};
}
}
cancel() {
this.isCancelled = true;
}
}
function noDelay() {
return Duration_1.Duration.ofMilliseconds(0);
}
function noEarlyExit() {
return false;
}
function rethrowErrors(error) {
throw error;
}
//# sourceMappingURL=Scheduler.js.map