@rushstack/operation-graph
Version:
Library for managing and executing operations in a directed acyclic graph.
213 lines • 8.15 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.WatchLoop = void 0;
const node_events_1 = require("node:events");
const node_core_library_1 = require("@rushstack/node-core-library");
const OperationStatus_1 = require("./OperationStatus");
/**
* This class implements a watch loop.
*
* @beta
*/
class WatchLoop {
constructor(options) {
/**
* Requests that a new run occur.
*/
this.requestRun = (requestor) => {
if (!this._runRequested) {
this._options.onRequestRun(requestor);
this._runRequested = true;
if (this._isRunning) {
this._options.onAbort();
this._abortCurrent();
}
}
this._resolveRequestRun(requestor);
};
/**
* Cancels the current iteration (if possible).
*/
this._abortCurrent = () => {
this._abortController.abort();
};
this._options = options;
this._abortController = new AbortController();
this._isRunning = false;
// Always start as true, so that any requests prior to first run are silenced.
this._runRequested = true;
this._requestRunPromise = new Promise((resolve) => {
this._resolveRequestRun = resolve;
});
}
/**
* Runs the inner loop until the abort signal is cancelled or a run completes without a new run being requested.
*/
async runUntilStableAsync(abortSignal) {
if (abortSignal.aborted) {
return OperationStatus_1.OperationStatus.Aborted;
}
abortSignal.addEventListener('abort', this._abortCurrent, { once: true });
try {
let result = OperationStatus_1.OperationStatus.Ready;
do {
// Always check the abort signal first, in case it was aborted in the async tick since the last executeAsync() call.
if (abortSignal.aborted) {
return OperationStatus_1.OperationStatus.Aborted;
}
result = await this._runIterationAsync();
} while (this._runRequested);
// Even if the run has finished, if the abort signal was aborted, we should return `Aborted` just in case.
return abortSignal.aborted ? OperationStatus_1.OperationStatus.Aborted : result;
}
finally {
abortSignal.removeEventListener('abort', this._abortCurrent);
}
}
/**
* Runs the inner loop until the abort signal is aborted. Will otherwise wait indefinitely for a new run to be requested.
*/
async runUntilAbortedAsync(abortSignal, onWaiting) {
if (abortSignal.aborted) {
return;
}
const abortPromise = (0, node_events_1.once)(abortSignal, 'abort');
// eslint-disable-next-line no-constant-condition
while (!abortSignal.aborted) {
await this.runUntilStableAsync(abortSignal);
onWaiting();
await Promise.race([this._requestRunPromise, abortPromise]);
}
}
/**
* Sets up an IPC handler that will run the inner loop when it receives a "run" message from the host.
* Runs until receiving an "exit" message from the host, or aborts early if an unhandled error is thrown.
*/
async runIPCAsync(host = process) {
await new Promise((resolve, reject) => {
let abortController = new AbortController();
let runRequestedFromHost = true;
let status = OperationStatus_1.OperationStatus.Ready;
function tryMessageHost(message) {
if (!host.send) {
return reject(new Error('Host does not support IPC'));
}
try {
host.send(message);
}
catch (err) {
reject(new Error(`Unable to communicate with host: ${err}`));
}
}
function requestRunFromHost(requestor) {
if (runRequestedFromHost) {
return;
}
runRequestedFromHost = true;
const requestRunMessage = {
event: 'requestRun',
requestor
};
tryMessageHost(requestRunMessage);
}
function sendSync() {
const syncMessage = {
event: 'sync',
status
};
tryMessageHost(syncMessage);
}
host.on('message', async (message) => {
switch (message.command) {
case 'exit': {
return resolve();
}
case 'cancel': {
if (this._isRunning) {
abortController.abort();
abortController = new AbortController();
// This will terminate the currently executing `runUntilStableAsync` call.
}
return;
}
case 'run': {
runRequestedFromHost = false;
status = OperationStatus_1.OperationStatus.Executing;
try {
status = await this.runUntilStableAsync(abortController.signal);
// ESLINT: "Promises must be awaited, end with a call to .catch, end with a call to .then ..."
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._requestRunPromise.finally(requestRunFromHost);
}
catch (err) {
status = OperationStatus_1.OperationStatus.Failure;
return reject(err);
}
finally {
const afterExecuteMessage = {
event: 'after-execute',
status
};
tryMessageHost(afterExecuteMessage);
}
return;
}
case 'sync': {
return sendSync();
}
default: {
return reject(new Error(`Unexpected command from host: ${message}`));
}
}
});
sendSync();
});
}
/**
* The abort signal for the current iteration.
*/
get abortSignal() {
return this._abortController.signal;
}
/**
* Resets the abort signal and run request state.
*/
_reset() {
if (this._abortController.signal.aborted) {
this._abortController = new AbortController();
}
if (this._runRequested) {
this._runRequested = false;
this._requestRunPromise = new Promise((resolve) => {
this._resolveRequestRun = resolve;
});
}
}
/**
* Runs a single iteration of the loop.
* @returns The status of the iteration.
*/
async _runIterationAsync() {
this._reset();
this._options.onBeforeExecute();
try {
this._isRunning = true;
return await this._options.executeAsync(this);
}
catch (err) {
if (!(err instanceof node_core_library_1.AlreadyReportedError)) {
throw err;
}
else {
return OperationStatus_1.OperationStatus.Failure;
}
}
finally {
this._isRunning = false;
}
}
}
exports.WatchLoop = WatchLoop;
//# sourceMappingURL=WatchLoop.js.map