mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
232 lines
5.7 KiB
JavaScript
232 lines
5.7 KiB
JavaScript
///
|
|
/// utility functions for computing progress of complex tasks
|
|
///
|
|
/// State callback here is an object with these keys:
|
|
/// done: bool, true if done
|
|
/// current: number, the current progress value
|
|
/// end: number, optional, the value of current where we expect to be done
|
|
///
|
|
/// If end is not set, we'll display a spinner instead of a progress bar
|
|
///
|
|
|
|
var _ = require('underscore');
|
|
|
|
var Progress = function (options) {
|
|
var self = this;
|
|
|
|
options = options || {};
|
|
|
|
self._lastState = null;
|
|
self._parent = options.parent;
|
|
self._watchers = options.watchers || [];
|
|
|
|
self._title = options.title;
|
|
if (self._title) {
|
|
// Capitalize job titles when displayed in the progress bar.
|
|
self._title = self._title[0].toUpperCase() + self._title.slice(1);
|
|
}
|
|
|
|
// XXX: Should we have a strict/mdg mode that enables this test?
|
|
//if (!self._title && self._parent) {
|
|
// throw new Error("No title passed");
|
|
//}
|
|
|
|
self._forkJoin = options.forkJoin;
|
|
|
|
self._allTasks = [];
|
|
|
|
self._selfState = { current: 0, done: false };
|
|
self._state = _.clone(self._selfState);
|
|
|
|
self._isDone = false;
|
|
|
|
self.startTime = +(new Date);
|
|
};
|
|
|
|
_.extend(Progress.prototype, {
|
|
toString: function() {
|
|
var self = this;
|
|
return "Progress [state=" + JSON.stringify(self._state) + "]";
|
|
},
|
|
|
|
reportProgressDone: function () {
|
|
var self = this;
|
|
|
|
var state = _.clone(self._selfState);
|
|
state.done = true;
|
|
if (state.end !== undefined) {
|
|
if (state.current > state.end) {
|
|
state.end = state.current;
|
|
}
|
|
state.current = state.end;
|
|
}
|
|
self.reportProgress(state);
|
|
},
|
|
|
|
// Tries to determine which is the 'current' job in the tree
|
|
// This is very heuristical... we use some hints, like:
|
|
// don't descend into fork-join jobs; we know these execute concurrently,
|
|
// so we assume the top-level task has the title
|
|
// i.e. "Downloading packages", not "downloading supercool-1.0"
|
|
getCurrentProgress: function () {
|
|
var self = this;
|
|
|
|
var isRoot = !self._parent;
|
|
|
|
if (self._isDone) {
|
|
// A done task cannot be the active task
|
|
return null;
|
|
}
|
|
|
|
if (!self._state.done && (self._state.current != 0) && self._state.end &&
|
|
!isRoot) {
|
|
// We are not done and we have interesting state to report
|
|
return self;
|
|
}
|
|
|
|
if (self._forkJoin) {
|
|
// Don't descend into fork-join tasks (by choice)
|
|
return self;
|
|
}
|
|
|
|
if (self._allTasks.length) {
|
|
var candidates = _.map(self._allTasks, function (task) {
|
|
return task.getCurrentProgress();
|
|
});
|
|
var active = _.filter(candidates, function (s) {
|
|
return !!s;
|
|
});
|
|
if (active.length) {
|
|
// pick one to display, somewhat arbitrarily
|
|
return active[active.length - 1];
|
|
}
|
|
// No single active task, return self
|
|
return self;
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
// Creates a subtask that must be completed as part of this (bigger) task
|
|
addChildTask: function (options) {
|
|
var self = this;
|
|
options = _.extend({ parent: self }, options || {});
|
|
var child = new Progress(options);
|
|
self._allTasks.push(child);
|
|
self._reportChildState(child, child._state);
|
|
return child;
|
|
},
|
|
|
|
// Dumps the tree, for debug
|
|
dump: function (stream, options, prefix) {
|
|
var self = this;
|
|
|
|
options = options || {};
|
|
if (options.skipDone && self._isDone) {
|
|
return;
|
|
}
|
|
|
|
if (prefix) {
|
|
stream.write(prefix);
|
|
}
|
|
var end = self._state.end;
|
|
if (!end) {
|
|
end = '?';
|
|
}
|
|
stream.write("Task [" + self._title + "] " + self._state.current + "/" + end
|
|
+ (self._isDone ? " done" : "") +"\n");
|
|
if (self._allTasks.length) {
|
|
_.each(self._allTasks, function (child) {
|
|
child.dump(stream, options, (prefix || '') + ' ');
|
|
});
|
|
}
|
|
},
|
|
|
|
// Receives a state report indicating progress of self
|
|
reportProgress: function (state) {
|
|
var self = this;
|
|
|
|
self._selfState = state;
|
|
|
|
self._updateTotalState();
|
|
|
|
// Nudge the spinner/progress bar, but don't yield (might not be safe to yield)
|
|
require('./console.js').Console.nudge(false);
|
|
|
|
self._notifyState();
|
|
},
|
|
|
|
// Subscribes a watcher to changes
|
|
addWatcher: function (watcher) {
|
|
var self = this;
|
|
|
|
self._watchers.push(watcher);
|
|
},
|
|
|
|
// Notifies watchers & parents
|
|
_notifyState: function () {
|
|
var self = this;
|
|
|
|
if (self._parent) {
|
|
self._parent._reportChildState(self, self._state);
|
|
}
|
|
|
|
if (self._watchers.length) {
|
|
_.each(self._watchers, function (watcher) {
|
|
watcher(self._state);
|
|
});
|
|
}
|
|
},
|
|
|
|
// Recomputes state, incorporating children's states
|
|
_updateTotalState: function () {
|
|
var self = this;
|
|
|
|
var allChildrenDone = true;
|
|
var state = _.clone(self._selfState);
|
|
_.each(self._allTasks, function (child) {
|
|
var childState = child._state;
|
|
|
|
if (!child._isDone) {
|
|
allChildrenDone = false;
|
|
}
|
|
|
|
state.current += childState.current;
|
|
if (state.end !== undefined) {
|
|
if (childState.done) {
|
|
state.end += childState.current;
|
|
} else if (childState.end !== undefined) {
|
|
state.end += childState.end;
|
|
} else {
|
|
state.end = undefined;
|
|
}
|
|
}
|
|
});
|
|
self._isDone = allChildrenDone && !!self._selfState.done;
|
|
if (!allChildrenDone) {
|
|
state.done = false;
|
|
}
|
|
|
|
if (!state.done && self._state.done) {
|
|
// This shouldn't happen
|
|
throw new Error("Progress transition from done => !done");
|
|
}
|
|
|
|
self._state = state;
|
|
},
|
|
|
|
// Called by a child when its state changes
|
|
_reportChildState: function (child, state) {
|
|
var self = this;
|
|
|
|
self._updateTotalState();
|
|
self._notifyState();
|
|
},
|
|
|
|
getState: function() {
|
|
return this._state;
|
|
}
|
|
});
|
|
|
|
exports.Progress = Progress;
|