import makeDeferred from './makeDeferred'; /** * Constants */ const TYPE = Symbol('Type'); const TASK = Symbol('Task'); const LIST = Symbol('List'); /** * Public Methods */ /** * Creates an instance of a task list * @returns {Object} A task list object */ function createList() { return objectWithType(LIST, { head: null, named: Object.create(null), observers: [], }); } /** * Checks if the given argument is a List instance * @param {any} subject The value to be tested * @returns {boolean} true if a valid List instance is given, false otherwise */ function isList(subject) { return isOfType(LIST, subject); } /** * Creates an instance of a task * @param {Object} list The List instance related to this task * @param {Object} next The next Task instance to link to * @returns {Object} A task object */ function createTask(list, next) { return objectWithType(TASK, { list: isList(list) ? list : null, next: isTask(next) ? next : null, failed: false, awaiting: null, progress: 0.0, }); } /** * Checks if the given argument is a Task instance * @param {any} subject The value to be tested * @returns {boolean} true if a valid Task instance is given, false otherwise */ function isTask(subject) { return isOfType(TASK, subject); } /** * Appends a new Task to the given List instance and notifies the list observers * @param {Object} list A List instance * @returns {Object} The new Task instance appended to the List or null if the * given List instanc is not valid */ function increaseList(list) { if (isList(list)) { const task = createTask(list, list.head); list.head = task; notify(list, getOverallProgress(list)); return task; } return null; } /** * Updates the internal progress value of the given Task instance and notifies * the observers of the associated list. * @param {Object} task The Task instance to be updated * @param {number} value A number between 0 (inclusive) and 1 (exclusive) * indicating the progress of the task; * @returns {void} Nothing is returned */ function update(task, value) { if (isTask(task) && isValidProgress(value) && value < 1.0) { if (task.progress !== value) { task.progress = value; if (isList(task.list)) { notify(task.list, getOverallProgress(task.list)); } } } } /** * Sets a Task instance as finished (progress = 1.0), freezes it in order to * prevent further modifications and notifies the observers of the associated * list. * @param {Object} task The Task instance to be finalized * @returns {void} Nothing is returned */ function finish(task) { if (isTask(task)) { task.progress = 1.0; task.awaiting = null; Object.freeze(task); if (isList(task.list)) { notify(task.list, getOverallProgress(task.list)); } } } /** * Generate a summarized snapshot of the current status of the given task List * @param {Object} list The List instance to be scanned * @returns {Object} An object representing the summarized status of the list */ function getOverallProgress(list) { const status = createStatus(); if (isList(list)) { let task = list.head; while (isTask(task)) { status.total++; if (isValidProgress(task.progress)) { status.partial += task.progress; if (task.progress === 1.0 && task.failed) { status.failures++; } } task = task.next; } } if (status.total > 0) { status.progress = status.partial / status.total; } return Object.freeze(status); } /** * Adds a Task instance to the given list that waits on a given "thenable". When * the thenable resolves the "finish" method is called on the newly created * instance thus notifying the observers of the list. * @param {Object} list The List instance to which the new task will be added * @param {Object|Promise} thenable The thenable to be waited on * @returns {Object} A reference to the newly created Task; */ function waitOn(list, thenable) { const task = increaseList(list); if (isTask(task)) { task.awaiting = Promise.resolve(thenable).then( function () { finish(task); }, function () { task.failed = true; finish(task); } ); return task; } return null; } /** * Adds a Task instance to the given list using a deferred (a Promise that can * be externally resolved) notifying the observers of the list. * @param {Object} list The List instance to which the new task will be added * @returns {Object} An object with references to the created deferred and task */ function addDeferred(list) { const deferred = makeDeferred(); const task = waitOn(list, deferred.promise); return Object.freeze({ deferred, task, }); } /** * Assigns a name to a specific task of the list * @param {Object} list The List instance whose task will be named * @param {Object} task The specified Task instance * @param {string} name The name of the task * @returns {boolean} Returns true on success, false otherwise */ function setTaskName(list, task, name) { if ( contains(list, task) && list.named !== null && typeof list.named === 'object' && typeof name === 'string' ) { list.named[name] = task; return true; } return false; } /** * Retrieves a task by name * @param {Object} list The List instance whose task will be retrieved * @param {string} name The name of the task to be retrieved * @returns {Object} The Task instance or null if not found */ function getTaskByName(list, name) { if ( isList(list) && list.named !== null && typeof list.named === 'object' && typeof name === 'string' ) { const task = list.named[name]; if (isTask(task)) { return task; } } return null; } /** * Adds an observer (callback function) to a given List instance * @param {Object} list The List instance to which the observer will be appended * @param {Function} observer The observer (function) that will be executed * every time a change happens within the list * @returns {boolean} Returns true on success and false otherwise */ function addObserver(list, observer) { if (isList(list) && Array.isArray(list.observers) && typeof observer === 'function') { list.observers.push(observer); return true; } return false; } /** * Removes an observer (callback function) from a given List instance * @param {Object} list The instance List from which the observer will removed * @param {Function} observer The observer function to be removed * @returns {boolean} Returns true on success and false otherwise */ function removeObserver(list, observer) { if (isList(list) && Array.isArray(list.observers) && list.observers.length > 0) { const index = list.observers.indexOf(observer); if (index >= 0) { list.observers.splice(index, 1); return true; } } return false; } /** * Utils */ function createStatus() { return Object.seal({ total: 0, partial: 0.0, progress: 0.0, failures: 0, }); } function objectWithType(type, object) { return Object.seal(Object.defineProperty(object, TYPE, { value: type })); } function isOfType(type, subject) { return subject !== null && typeof subject === 'object' && subject[TYPE] === type; } function isValidProgress(value) { return typeof value === 'number' && value >= 0.0 && value <= 1.0; } function contains(list, task) { if (isList(list) && isTask(task)) { let item = list.head; while (isTask(item)) { if (item === task) { return true; } item = item.next; } } return false; } function notify(list, data) { if (isList(list) && Array.isArray(list.observers) && list.observers.length > 0) { list.observers.slice().forEach(function (observer) { if (typeof observer === 'function') { try { observer(data, list); } catch (e) { /* Oops! */ } } }); } } /** * Exports */ const progressTrackingUtils = { createList, isList, createTask, isTask, increaseList, update, finish, getOverallProgress, waitOn, addDeferred, setTaskName, getTaskByName, addObserver, removeObserver, }; export default progressTrackingUtils;