UNPKG

alchemymvc

Version:
764 lines (607 loc) 14.4 kB
const STATUS = Symbol('status'), STATUS_PLEDGE = Symbol('status_pledge'), MAIN_PLEDGE = Symbol('main_pledge'), PRE_STATUS = 'pre', MAIN_STATUS = 'main', CHILD_STATUS = 'children', POST_STATUS = 'post'; /** * The Stage class * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} name * @param {Alchemy.Stages.Stages} parent */ const Stage = Function.inherits('Alchemy.Base', 'Alchemy.Stages', function Stage(name, parent) { // The name of this stage this.name = name; // The path this.id = parent ? parent.id + '.' + name : name; // The parent Stage (if any) this.parent = parent; // The root stage this.root_stage = parent ? parent.root_stage : this; // The current status this[STATUS] = null; // The main pledge this[MAIN_PLEDGE] = new Pledge.Swift(); // The dependencies of this stage this.depends_on = null; // Pre-tasks this.pre_tasks = new Map(); // Main tasks this.main_tasks = new Map(); // Child stages this.child_stages = new Map(); // Post-tasks this.post_tasks = new Map(); // When this started this.started = null; // When this ended this.ended = null; }); /** * Is this the root stage? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 */ Stage.setProperty(function is_root() { return this.root_stage === this; }); /** * Get the main pledge * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 */ Stage.setProperty(function pledge() { return this[MAIN_PLEDGE]; }); /** * Add a dependency to this stage * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} stage_ids The id of the stage it depends on. * * @return {Alchemy.Stages.Stage} */ Stage.setMethod(function dependsOn(stage_ids) { if (!this.depends_on) { this.depends_on = []; } stage_ids = Array.cast(stage_ids); for (let i = 0; i < stage_ids.length; i++) { let id = stage_ids[i]; if (typeof id != 'string') { throw new Error('Stage id should be a string'); } if (!this.is_root && !id.startsWith(this.root_stage.name)) { id = this.root_stage.name + '.' + id; } stage_ids[i] = id; } this.depends_on.push(...stage_ids); // Push these dependencies to the already existing children for (let [name, stage] of this.child_stages) { stage.dependsOn(stage_ids); } return this; }); /** * Add a new child stage * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} name Name of the stage * @param {Function} fnc The function to execute as a main task * * @return {Alchemy.Stages.Stage} */ Stage.setMethod(function createStage(name, fnc) { if (this.child_stages.has(name)) { throw new Error('Stage "' + name + '" already exists'); } let stage = new Stage(name, this); if (this.depends_on?.length) { stage.dependsOn(this.depends_on); } this.child_stages.set(name, stage); if (fnc) { stage.addMainTask(fnc); } return stage; }); /** * Add a certain task * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} type * @param {Function} fnc */ Stage.setMethod(function _addTask(type, fnc) { let task_map = this[type]; // First see if this type has already been started let pledge = task_map[STATUS_PLEDGE]; // If it has, we can already start the task, // but we have to set a new pledge. if (pledge) { let new_pledge = new Pledge.Swift(); task_map[STATUS_PLEDGE] = new_pledge; let task_result; try { task_result = fnc(); } catch (err) { new_pledge.reject(err); task_map.set(fnc, err); return; } if (!task_result) { task_result = true; } task_map.set(fnc, task_result); } else { task_map.set(fnc, null); } }); /** * Do the given type of tasks * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} type */ Stage.setMethod(function _doTasks(type) { let task_map = this[type]; let pledge = new Pledge.Swift(); task_map[STATUS_PLEDGE] = pledge; let tasks = []; let errors = []; for (let [fnc, value] of task_map) { // It already has a value: it has already been executed if (value) { if (Pledge.isThenable(value)) { tasks.push(value); } continue; } // If needs to be executed try { value = fnc(); if (!value) { value = true; } else if (Pledge.isThenable(value)) { tasks.push(value); Pledge.cast(value).done((err, result) => { task_map.set(fnc, result || err || true); }); } task_map.set(fnc, value); } catch (err) { errors.push(err); task_map.set(fnc, err); } } if (!tasks.length && !errors.length) { pledge.resolve(true); return pledge; } if (errors.length) { pledge.reject(errors[0]); return pledge; } Function.parallel(tasks).done((err) => { if (err) { pledge.reject(err); } else { pledge.resolve(this._doTasks(type)); } }); return pledge; }); /** * Add a pre-task to this stage: * This task will be performed before the main tasks of this stage. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {Function} fnc */ Stage.setMethod(function addPreTask(fnc) { this._addTask('pre_tasks', fnc); }); /** * Add a main task to this stage: * This task will be performed after the pre-tasks of this stage. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {Function} fnc */ Stage.setMethod(function addMainTask(fnc) { this._addTask('main_tasks', fnc); }); /** * Add a post task to this stage: * This task will be performed after the main-tasks * and the child stages of this stage. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {Function} fnc */ Stage.setMethod(function addPostTask(fnc) { this._addTask('post_tasks', fnc); }); /** * Have all the child stages finished? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 */ Stage.setMethod(function hasFinishedAllChildStages() { if (this.child_stages.size == 0) { return true; } for (let [name, stage] of this.child_stages) { if (!stage.ended) { return false; } } return true; }); /** * Get a child stage by its path/id * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} id */ Stage.setMethod(function getStage(id) { if (!id) { throw new Error('Unable to get stage without id'); } let parts = id.split('.'); if (this.is_root && parts[0] == this.name) { parts.shift(); } let current = this; while (parts.length) { let part = parts.shift(); current = current.child_stages.get(part); if (!current) { return; } } return current; }); /** * Wait for the given child stages (without starting them) * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} stages * @param {Function} callback */ Stage.setMethod(function afterStages(stages, callback) { stages = Array.cast(stages); let tasks = []; for (let id of stages) { let stage = this.getStage(id); if (!stage) { throw new Error('Child stage "' + id + '" not found'); } if (stage.ended) { continue; } tasks.push(async (next) => { await stage.pledge; next(); }); } return Function.series(tasks, callback); }); /** * Recursively get all the child-stages (including this one) * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} filter * * @return {Alchemy.Stage.Stage[]} */ Stage.setMethod(function getFlattenedStages(filter) { let result = []; result.push(this); // If the filter is given, split each entry up into parts if (filter) { filter = Array.cast(filter); for (let i = 0; i < filter.length; i++) { let parts = filter[i].split('.'); if (this.is_root && parts[0] == this.name) { parts.shift(); } filter[i] = parts; } } for (let [name, stage] of this.child_stages) { let sub_filter; if (filter) { let matches_filter = false; sub_filter = []; for (let i = 0; i < filter.length; i++) { let parts = filter[i]; if (parts[0] == name) { matches_filter = true; // Create a clone of the parts array parts = parts.slice(0); // Remove the first part parts.shift(); if (parts.length) { // And add it to the sub filter sub_filter.push(parts); } } } if (!matches_filter) { continue; } if (!sub_filter.length) { sub_filter = null; } } result.push(...stage.getFlattenedStages(sub_filter)); } return result; }); /** * Recursively get all the child-stages (including this one) * sorted in launch order. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} filter * * @return {Alchemy.Stage.Stage[]} */ Stage.setMethod(function getSortedStages(filter) { let stages = this.getFlattenedStages(filter); stages.sortTopological('id', 'depends_on'); return stages; }); /** * Launch this stage and all the given child stages. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} child_stages The child stages to launch */ Stage.setMethod(async function launch(child_stages) { if (child_stages == null) { throw new Error('Unable to launch a stage without allowed child stages'); } if (child_stages === true) { child_stages = undefined; } let stages = this.getSortedStages(child_stages); for (let stage of stages) { await stage._launch(); } }); /** * Actually launch this stage and all the given child stages * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} child_stages The child stages to launch */ Stage.setMethod(async function _launch(child_stages) { if (!this.started) { this.started = Date.now(); this.emit('launching', this); if (!this.is_root) { this.root_stage.emit('launching', this); } setTimeout(() => { if (!this.ended) { console.warn('Stage "' + this.id + '" is taking a long time...'); } }, 10 * 1000); } if (!this[STATUS]) { this[STATUS] = PRE_STATUS; } await this._doTasks('pre_tasks'); if (this[STATUS] == PRE_STATUS) { this[STATUS] = MAIN_STATUS; } await this._doTasks('main_tasks'); await this.pre_tasks[STATUS_PLEDGE]; await this.main_tasks[STATUS_PLEDGE]; if (this[STATUS] != POST_STATUS) { this[STATUS] = CHILD_STATUS; } await this.refreshStatus(); }); /** * Check if everything is finished * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 */ Stage.setMethod(async function refreshStatus() { if (!this.hasFinishedAllChildStages()) { return; } await this._doTasks('post_tasks'); if (!this.ended) { this.ended = Date.now(); } this[STATUS] = POST_STATUS; this.pledge.resolve(); if (this.parent) { this.parent.refreshStatus(); } }); /** * Create a sputnik shim * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {Object} mapping * * @return {Alchemy.Stages.SputnikShim} */ Stage.setMethod(function createSputnikShim(mapping) { return new SputnikShim(this, mapping); }); /** * Custom Janeway representation (left side) * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @return {string} */ Stage.setMethod(Symbol.for('janeway_arg_left'), function janewayClassIdentifier() { return 'A.S.' + this.constructor.name; }); /** * Custom Janeway representation (right side) * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @return {String} */ Stage.setMethod(Symbol.for('janeway_arg_right'), function janewayInstanceInfo() { return this.id; }); /** * The SputnikShim class * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {Alchemy.Stages.Stages} stage * @param {Object} mapping */ const SputnikShim = Function.inherits('Alchemy.Base', 'Alchemy.Stages', function SputnikShim(stage, mapping) { this.stage = stage; this.mapping = mapping; }); /** * Get a stage by its old name * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} name * * @return {Alchemy.Stages.Stage} */ SputnikShim.setMethod(function getStage(name) { if (!name) { throw new Error('Unable to get stage without name'); } let id = this.mapping[name]; if (!id) { id = name; } return this.stage.getStage(id); }); /** * Do something before the given stage * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} names * @param {Function} callback */ SputnikShim.setMethod(function before(names, callback) { let pledges = [], stage; names = Array.cast(names); for (let name of names) { stage = this.getStage(name); if (!stage) { throw new Error('Stage "' + name + '" not found'); } let pledge = new Pledge.Swift(); stage.addPreTask(() => { pledge.resolve(); }); pledges.push(pledge); } return Function.parallel(pledges).then(callback); }); /** * Do something after the given stage * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string[]} names * @param {Function} callback */ SputnikShim.setMethod(function after(names, callback) { let pledges = [], stage; names = Array.cast(names); for (let name of names) { stage = this.getStage(name); if (!stage) { throw new Error('Stage "' + name + '" not found'); } let pledge = new Pledge.Swift(); stage.addPostTask(() => { pledge.resolve(); }); pledges.push(pledge); } return Function.parallel(pledges).then(callback); });