UNPKG

@rushstack/operation-graph

Version:

Library for managing and executing operations in a directed acyclic graph.

213 lines 8.15 kB
"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