@sfdx-falcon/task
Version:
Specialized abstraction of a single Listr Task. Part of the SFDX-Falcon Library.
406 lines • 27.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const rxjs_1 = require("rxjs"); // Class. Used to communicate status with Listr.
const rxjs_2 = require("rxjs"); // Class. Implements the Observer interface and extends the Subscription class.
// Import SFDX-Falcon Libraries
const util_1 = require("@sfdx-falcon/util"); // Library. Async utility helper functions.
const validator_1 = require("@sfdx-falcon/validator"); // Library of Type Validation helper functions.
// Import SFDX-Falcon Classes & Functions
const debug_1 = require("@sfdx-falcon/debug"); // Class. Provides custom "debugging" services (ie. debug-style info to console.log()).
const error_1 = require("@sfdx-falcon/error"); // Class. Extends SfdxError to provide specialized error structures for SFDX-Falcon modules.
const status_1 = require("@sfdx-falcon/status"); // Class. Status tracking object for use with Generators derived from SfdxFalconGenerator.
const status_2 = require("@sfdx-falcon/status"); // Class. Implements a framework for creating results-driven, informational objects with a concept of heredity (child results) and the ability to "bubble up" both Errors (thrown exceptions) and application-defined "failures".
const status_3 = require("@sfdx-falcon/status"); // Class. Manages status notification messages for SFDX-Falcon Tasks.
const status_4 = require("@sfdx-falcon/status"); // Class. Simplifies access to a TaskStatusOptions data structure to help ensure that an external caller can read/set messsages in a way that won't break normal functionality.
// Set the File Local Debug Namespace
const dbgNs = '@sfdx-falcon:task';
debug_1.SfdxFalconDebug.msg(`${dbgNs}:`, `Debugging initialized for ${dbgNs}`);
//─────────────────────────────────────────────────────────────────────────────────────────────────┐
/**
* @class ObservableTaskResult
* @description Creates the structure needed to wrap the output of any task as an `SfdxFalconResult`
* while also managing access to the Observer that Listr uses to monitor the progress
* of a task.
* @public
*/
//─────────────────────────────────────────────────────────────────────────────────────────────────┘
class ObservableTaskResult {
//───────────────────────────────────────────────────────────────────────────┐
/**
* @constructs ObservableTaskResult
* @param {ObservableTaskResultOptions} opts Required. Options that
* determine how this `ObservableTaskResult` will be constructed.
* @description Constructs an `ObservableTaskResult` object.
* @public
*/
//───────────────────────────────────────────────────────────────────────────┘
constructor(opts) {
// Set function-local debug namespace and examine incoming arguments.
const dbgNsLocal = `${dbgNs}:ObservableTaskResult:constructor`;
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:arguments:`, arguments);
// Validate the REQUIRED contents of the options object.
validator_1.TypeValidator.throwOnEmptyNullInvalidObject(opts, `${dbgNsLocal}`, `ObservableTaskResultOptions`);
validator_1.TypeValidator.throwOnNullInvalidObject(opts.extCtx, `${dbgNsLocal}`, `ObservableTaskResultOptions.extCtx`);
validator_1.TypeValidator.throwOnEmptyNullInvalidString(opts.extCtx.dbgNs, `${dbgNsLocal}`, `ObservableTaskResultOptions.extCtx.dbgNs`);
validator_1.TypeValidator.throwOnNullInvalidObject(opts.taskCtx, `${dbgNsLocal}`, `ObservableTaskResultOptions.taskCtx`);
validator_1.TypeValidator.throwOnEmptyNullInvalidObject(opts.taskObj, `${dbgNsLocal}`, `ObservableTaskResultOptions.taskObj`);
validator_1.TypeValidator.throwOnEmptyNullInvalidObject(opts.subscriber, `${dbgNsLocal}`, `ObservableTaskResultOptions.subscriber`);
validator_1.TypeValidator.throwOnNullInvalidString(opts.statusMsg, `${dbgNsLocal}`, `ObservableTaskResultOptions.statusMsg`);
validator_1.TypeValidator.throwOnNullInvalidNumber(opts.minRuntime, `${dbgNsLocal}`, `ObservableTaskResultOptions.minRuntime`);
validator_1.TypeValidator.throwOnNullInvalidBoolean(opts.showTimer, `${dbgNsLocal}`, `ObservableTaskResultOptions.showTimer`);
// Validate the OPTIONAL contents of the External Context options object.
if (opts.extCtx.sharedData)
validator_1.TypeValidator.throwOnNullInvalidObject(opts.extCtx.sharedData, `${dbgNsLocal}`, `ObservableTaskResultOptions.extCtx.sharedData`);
if (opts.extCtx.parentResult)
validator_1.TypeValidator.throwOnNullInvalidInstance(opts.extCtx.parentResult, status_2.SfdxFalconResult, `${dbgNsLocal}`, `ObservableTaskResultOptions.extCtx.parentResult`);
if (opts.extCtx.generatorStatus)
validator_1.TypeValidator.throwOnNullInvalidInstance(opts.extCtx.generatorStatus, status_1.GeneratorStatus, `${dbgNsLocal}`, `ObservableTaskResultOptions.extCtx.generatorStatus`);
// Validate the DEEPER contents of the options object.
validator_1.TypeValidator.throwOnNullInvalidInstance(opts.subscriber, rxjs_2.Subscriber, `${dbgNsLocal}`, `ObservableTaskResultOptions.subscriber`);
// Initialize member variables.
this._extCtx = opts.extCtx;
this._taskCtx = opts.taskCtx;
this._taskObj = opts.taskObj;
this._subscriber = opts.subscriber;
this._minRunTime = opts.minRuntime;
this._showTimer = opts.showTimer;
// Initialize the Task Status Messages object.
this._taskStatusMessages = {
defaultMessage: opts.statusMsg,
currentMessage: opts.statusMsg,
showTimer: this._showTimer,
prevMessage: null,
nextMessage: null
};
// Initialize the Task Status Message object.
this._taskStatusMessage = new status_4.TaskStatusMessage(this._taskStatusMessages);
// Initialize an SFDX-Falcon Result object.
this._taskResult = new status_2.SfdxFalconResult(this._extCtx.dbgNs, "TASK" /* TASK */, { startNow: true,
bubbleError: false,
bubbleFailure: false }); // Let the parent Result handle failures (no bubbling)
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:_taskResult:`, this._taskResult);
// Initialize the Task Result Detail.
this._taskResultDetail = {
extCtx: this._extCtx,
taskCtx: this._taskCtx,
taskObj: this._taskObj,
statusMsgs: this._taskStatusMessages
};
this._taskResult.setDetail(this._taskResultDetail);
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:_taskResult.detail:`, this._taskResult.detail);
// Set the initial Task Detail message.
if (this._showTimer) {
this._subscriber.next(`[0s] ${this._taskStatusMessages.defaultMessage}`);
}
else {
if (this._taskStatusMessages.defaultMessage) {
this._subscriber.next(`${this._taskStatusMessages.defaultMessage}`);
}
}
// Set up Task Progress Notifications and store a reference to the Timer.
this._notificationTimer = status_3.TaskStatus.start(this._taskStatusMessages, 1000, this._taskResult, this._subscriber);
}
// Public accessors
/** Represents the Status Message for the associated `SfdxFalconTask`. Allows external logic to read and set status messages. */
get status() {
return this._taskStatusMessage;
}
//───────────────────────────────────────────────────────────────────────────┐
/**
* @method finalizeFailure
* @param {ErrorOrResult} errorOrResult Required.
* @returns {void}
* @description Finalizes this `ObservableTaskResult` in a manner that
* indicates the associated task was NOT successful.
* @public
*/
//───────────────────────────────────────────────────────────────────────────┘
finalizeFailure(errorOrResult) {
// Set function-local debug namespace and examine incoming arguments.
const dbgNsLocal = `${dbgNs}:ObservableTaskResult:finalizeFailure`;
debug_1.SfdxFalconDebug.msg(`${dbgNsLocal}:`, `finalizeFailure() has been called`);
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:arguments:`, arguments);
// Define a special "subscriber throw" function.
const subscriberThrow = (error) => {
try {
this._subscriber.error(error_1.SfdxFalconError.wrap(error));
}
catch (noSubscriberError) {
// The only reason we should get here is if this._subscriber lost its Subscriber reference.
// If that happens, handle it gracefully by just sending a Debug message and nothing else.
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:subscriberThrow:noSubscriberError`, noSubscriberError);
}
};
// Validate incoming arguments.
if (validator_1.TypeValidator.isEmptyNullInvalidObject(errorOrResult)) {
return subscriberThrow(new error_1.SfdxFalconError(`${validator_1.TypeValidator.errMsgEmptyNullInvalidObject(errorOrResult, `errorOrResult`)}`, `TypeError`, `${dbgNsLocal}`));
}
if (validator_1.TypeValidator.isInvalidInstance(errorOrResult, Error)
&& validator_1.TypeValidator.isInvalidInstance(errorOrResult, status_2.SfdxFalconResult)) {
return subscriberThrow(new error_1.SfdxFalconError(`Expected errorOrResult to be an instance of Error or SfdxFalconResult but got '${(errorOrResult.constructor) ? errorOrResult.constructor.name : 'unknown'}' instead.`, `TypeError`, `${dbgNsLocal}`));
}
// Validate current state of key instance variables.
if (validator_1.TypeValidator.isInvalidInstance(this._taskResult, status_2.SfdxFalconResult)) {
return subscriberThrow(new error_1.SfdxFalconError(`${validator_1.TypeValidator.errMsgInvalidInstance(this._taskResult, status_2.SfdxFalconResult, `this._taskResult`)}`, `TypeError`, `${dbgNsLocal}`));
}
if (validator_1.TypeValidator.isNullInvalidObject(this._taskResult.detail)) {
return subscriberThrow(new error_1.SfdxFalconError(`${validator_1.TypeValidator.errMsgNullInvalidObject(this._taskResult.detail, `this._taskResult.detail`)}`, `TypeError`, `${dbgNsLocal}`));
}
// Finish any Task Progress Notifications attached to this Observable Task Result.
status_3.TaskStatus.finish(this._notificationTimer);
// Set the final ERROR state of the Task Result.
if (errorOrResult instanceof Error) {
this._taskResult.error(errorOrResult);
}
if (errorOrResult instanceof status_2.SfdxFalconResult) {
try {
this._taskResult.addChild(errorOrResult);
}
catch (_a) {
// No need to do anything here. We are just suppressing any
// bubbled errors from the previous addChild() call.
}
}
// Add the Task Result as a child of the Parent Result (if present).
if (this._extCtx.parentResult instanceof status_2.SfdxFalconResult) {
try {
this._extCtx.parentResult.addChild(this._taskResult);
}
catch (bubbledError) {
// If we get here, it means the parent was set to Bubble Errors.
// That means that bubbledError should be an SfdxFalconResult
return subscriberThrow(error_1.SfdxFalconError.wrap(bubbledError.errObj));
}
}
// Finalize the Subscriber with "error"
if (errorOrResult instanceof Error) {
return subscriberThrow(error_1.SfdxFalconError.wrap(errorOrResult));
}
else {
if (validator_1.TypeValidator.isNotInvalidInstance(errorOrResult.errObj, Error)) {
return subscriberThrow(error_1.SfdxFalconError.wrap(errorOrResult.errObj));
}
else {
const unexpectedError = new error_1.SfdxFalconError(`Received an Error Result that did not contain an Error Object. See error.detail for more information.`, `MissingErrObj`, `${dbgNsLocal}`, null, errorOrResult);
return subscriberThrow(unexpectedError);
}
}
}
//───────────────────────────────────────────────────────────────────────────┐
/**
* @method finalizeSuccess
* @returns {void}
* @description Finalizes this `ObservableTaskResult` in a manner that
* indicates the associated task was successful.
* @public
*/
//───────────────────────────────────────────────────────────────────────────┘
finalizeSuccess() {
// Set function-local debug namespace.
const dbgNsLocal = `${dbgNs}:ObservableTaskResult:finalizeSuccess`;
debug_1.SfdxFalconDebug.msg(`${dbgNsLocal}:`, `finalizeSuccess() has been called`);
// Finish any Task Progress Notifications attached to this Observable Task Result.
status_3.TaskStatus.finish(this._notificationTimer);
// Set the final SUCCESS state of the Task Result.
this._taskResult.success();
// Add the Task Result as a child of the Parent Result (if present).
if (this._extCtx.parentResult instanceof status_2.SfdxFalconResult) {
try {
this._extCtx.parentResult.addChild(this._taskResult);
}
catch (bubbledError) {
// If we get here, it means the parent was set to Bubble Errors.
// It also means that the Task Result was NOT successful, despite
// the fact that somebody called the finalizeSuccess() method.
// TODO: Not sure what the right behavior should be here.
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}`, bubbledError);
}
}
// Finalize the Subscriber with "complete"
try {
this._subscriber.complete();
debug_1.SfdxFalconDebug.msg(`${dbgNsLocal}`, `Subscriber.complete() has been called`);
}
catch (noSubscriberError) {
// The only reason we should get here is if this._subscriber lost its Subscriber reference.
// If that happens, handle it gracefully by just sending a Debug message and nothing else.
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:noSubscriberError:`, noSubscriberError);
}
}
//───────────────────────────────────────────────────────────────────────────┐
/**
* @method remainingRuntime
* @returns {number}
* @description Finds out how long it's been since the Task Result started and
* subtracts that value from the minimum runtime (`_minRunTime`)
* to compute the remaining runtime.
* @public
*/
//───────────────────────────────────────────────────────────────────────────┘
remainingRuntime() {
const currentRuntime = this._taskResult.durationSecs;
const remainingRuntime = this._minRunTime - currentRuntime;
return (remainingRuntime <= 0) ? 0 : Math.ceil(remainingRuntime);
}
}
exports.ObservableTaskResult = ObservableTaskResult;
//─────────────────────────────────────────────────────────────────────────────────────────────────┐
/**
* @class SfdxFalconTask
* @description Abstraction of a single Listr Task with a lot of extra functionality bundled in.
* @public
*/
//─────────────────────────────────────────────────────────────────────────────────────────────────┘
class SfdxFalconTask {
//───────────────────────────────────────────────────────────────────────────┐
/**
* @constructs SfdxFalconTask
* @param {SfdxFalconTaskOptions} opts Required. Options used to
* construct this instance of an `SfdxFalconTask` object.
* @description Constructs an `SfdxFalconTask` object.
* @public
*/
//───────────────────────────────────────────────────────────────────────────┘
constructor(opts) {
// Set function-local debug namespace and examine incoming arguments.
const dbgNsLocal = `${dbgNs}:SfdxFalconTask:constructor`;
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:arguments:`, arguments);
// Make sure the caller passed in an options object.
validator_1.TypeValidator.throwOnEmptyNullInvalidObject(opts, `${dbgNsLocal}`, `opts`);
// Build a set of resolved options by mixing what the caller supplied with our defaults.
const resolvedOpts = Object.assign({ extCtxReqs: {
sharedData: false,
parentResult: false,
generatorStatus: false
}, statusMsg: '', minRuntime: 0, showTimer: false }, opts // Mixin the options provided by the caller.
);
// Validate the contents of the resolved options.
validator_1.TypeValidator.throwOnEmptyNullInvalidString(resolvedOpts.title, `${dbgNsLocal}`, `SfdxFalconTaskOptions.title`);
validator_1.TypeValidator.throwOnNullInvalidFunction(resolvedOpts.task, `${dbgNsLocal}`, `SfdxFalconTaskOptions.task`);
if (typeof resolvedOpts.skip !== 'undefined')
validator_1.TypeValidator.throwOnNullInvalidFunction(resolvedOpts.skip, `${dbgNsLocal}`, `SfdxFalconTaskOptions.skip`);
if (typeof resolvedOpts.enabled !== 'undefined')
validator_1.TypeValidator.throwOnNullInvalidFunction(resolvedOpts.enabled, `${dbgNsLocal}`, `SfdxFalconTaskOptions.enabled`);
validator_1.TypeValidator.throwOnNullInvalidObject(resolvedOpts.extCtx, `${dbgNsLocal}`, `SfdxFalconTaskOptions.extCtx`);
validator_1.TypeValidator.throwOnEmptyNullInvalidString(resolvedOpts.extCtx.dbgNs, `${dbgNsLocal}`, `SfdxFalconTaskOptions.extCtx.dbgNs`);
validator_1.TypeValidator.throwOnEmptyNullInvalidObject(resolvedOpts.extCtxReqs, `${dbgNsLocal}`, `SfdxFalconTaskOptions.extCtxReqs`);
if (resolvedOpts.extCtxReqs.sharedData || typeof resolvedOpts.extCtx.sharedData !== 'undefined')
validator_1.TypeValidator.throwOnNullInvalidObject(resolvedOpts.extCtx.sharedData, `${dbgNsLocal}`, `SfdxFalconTaskOptions.extCtx.sharedData`);
if (resolvedOpts.extCtxReqs.parentResult || typeof resolvedOpts.extCtx.parentResult !== 'undefined')
validator_1.TypeValidator.throwOnNullInvalidInstance(resolvedOpts.extCtx.parentResult, status_2.SfdxFalconResult, `${dbgNsLocal}`, `SfdxFalconTaskOptions.extCtx.parentResult`);
if (resolvedOpts.extCtxReqs.generatorStatus || typeof resolvedOpts.extCtx.generatorStatus !== 'undefined')
validator_1.TypeValidator.throwOnNullInvalidInstance(resolvedOpts.extCtx.generatorStatus, status_1.GeneratorStatus, `${dbgNsLocal}`, `SfdxFalconTaskOptions.extCtx.generatorStatus`);
validator_1.TypeValidator.throwOnNullInvalidString(resolvedOpts.statusMsg, `${dbgNsLocal}`, `SfdxFalconTaskOptions.statusMsg`);
validator_1.TypeValidator.throwOnNullInvalidNumber(resolvedOpts.minRuntime, `${dbgNsLocal}`, `SfdxFalconTaskOptions.minRuntime`);
validator_1.TypeValidator.throwOnNullInvalidBoolean(resolvedOpts.showTimer, `${dbgNsLocal}`, `SfdxFalconTaskOptions.showTimer`);
// Initialize Listr-specific member variables.
this._title = resolvedOpts.title;
this._extTask = resolvedOpts.task;
this._extSkip = resolvedOpts.skip;
this._extEnabled = resolvedOpts.enabled;
// Initialize SFDX-Falcon Task-specific variables.
this._extCtx = resolvedOpts.extCtx;
this._statusMsg = resolvedOpts.statusMsg;
this._minRuntime = resolvedOpts.minRuntime;
this._showTimer = resolvedOpts.showTimer;
}
//───────────────────────────────────────────────────────────────────────────┐
/**
* @method build
* @returns {ListrTask}
* @description Builds an Observable `ListrTask` object based on the info
* that the caller provided to us when this `SfdxFalconTask`
* object was constructed.
* @public
*/
//───────────────────────────────────────────────────────────────────────────┘
build() {
// Set function-local debug namespace.
const dbgNsLocal = `${dbgNs}:SfdxFalconTask:build`;
// Build the specialized Observer based task.
this._task = (taskCtx, taskObj) => {
return new rxjs_1.Observable((subscriber) => {
// Initialize an OTR (Observable Task Result).
this._otr = new ObservableTaskResult({
taskCtx: taskCtx,
taskObj: taskObj,
subscriber: subscriber,
extCtx: this._extCtx,
statusMsg: this._statusMsg,
minRuntime: this._minRuntime,
showTimer: this._showTimer
});
// Initialize the External Task that's associated with this instance.
let extTaskPromise;
try {
// Hoist the External Task function into the Listr task we're creating.
extTaskPromise = this._extTask(taskCtx, taskObj, this._otr.status, this._extCtx);
}
catch (error) {
const extTaskSetupError = new error_1.SfdxFalconError(`External Task could not be initialized. ${error.message}`, `TaskFunctionInitError`, `${dbgNsLocal}`, error, this._extTask);
return this._otr.finalizeFailure(extTaskSetupError);
}
// Make sure the External Task returned a Promise.
if ((extTaskPromise instanceof Promise) !== true) {
const invalidExtTaskFunctionError = new error_1.SfdxFalconError(`SFDX-Falcon Task Functions must be asynchronous and return Promise<void>`, `InvalidTaskFunction`, `${dbgNsLocal}`, null, this._extTask);
return this._otr.finalizeFailure(invalidExtTaskFunctionError);
}
// Set THEN and CATCH callbacks for the External Task.
extTaskPromise
.then(async () => {
debug_1.SfdxFalconDebug.msg(`${dbgNsLocal}:`, `extTaskPromise() has been RESOVLED.`);
// Make sure the task appears to run for the minimum runtime.
await util_1.AsyncUtil.waitASecond(this._otr.remainingRuntime());
// Finalize the OTR as a SUCCESS.
return this._otr.finalizeSuccess();
})
.catch(async (rejectedResult) => {
debug_1.SfdxFalconDebug.msg(`${dbgNsLocal}:`, `extTaskPromise() has been REJECTED.`);
debug_1.SfdxFalconDebug.obj(`${dbgNsLocal}:rejectedResult:`, { rejectedResult: rejectedResult });
// Make sure the task appears to run for the minimum runtime.
await util_1.AsyncUtil.waitASecond(this._otr.remainingRuntime());
// Declare variables used to construct an Error.
let errorMessage;
let errorCause;
let errorDetail;
// Determine the Error Message, Cause, and Detail based on what kind of Rejected Result we got.
if (rejectedResult instanceof status_2.SfdxFalconResult) {
errorMessage = `Task Function failed and returned the SfdxFalconResult '${rejectedResult.name}'.`;
errorCause = rejectedResult.errObj;
errorDetail = rejectedResult;
}
else if (rejectedResult instanceof Error) {
errorMessage = `Task Function failed with the following error: ${rejectedResult.message}`;
errorCause = rejectedResult;
errorDetail = {};
}
else {
errorMessage = `Task Function failed with an unexpected result.`;
errorCause = undefined;
errorDetail = { rejectedResult: rejectedResult };
}
// Construct the Task Error.
const taskError = new error_1.SfdxFalconError(`${errorMessage}`, `TaskFunctionError`, `${dbgNsLocal}`, errorCause, errorDetail);
// Finalize the OTR as a FAILURE.
return this._otr.finalizeFailure(taskError);
});
});
};
// Build the SKIP and ENABLED functions
if (typeof this._extSkip === 'function') {
this._skip = (taskCtx) => this._extSkip(taskCtx, this._extCtx);
}
if (typeof this._extEnabled === 'function') {
this._enabled = (taskCtx) => this._extEnabled(taskCtx, this._extCtx);
}
// Construct and return a ListrTask
return {
title: this._title,
task: this._task,
enabled: this._enabled,
skip: this._skip
};
}
}
exports.SfdxFalconTask = SfdxFalconTask;
//# sourceMappingURL=task.js.map