From c1b355e0d3d7bf48e7ce865f6e24b9413848581f Mon Sep 17 00:00:00 2001 From: danschultz Date: Mon, 7 Mar 2011 17:58:26 -0800 Subject: [PATCH] adding a queue to manage the execution of operations [#29 state:resolved] --- src/operations/CompoundOperation.as | 9 + src/operations/Operation.as | 109 +++++++++- src/operations/OperationQueue.as | 259 +++++++++++++++++++++++ src/operations/Progress.as | 59 ++++++ src/operations/ProgressOperationEvent.as | 41 ++++ src/operations/QueueProgress.as | 36 ++++ 6 files changed, 508 insertions(+), 5 deletions(-) create mode 100644 src/operations/OperationQueue.as create mode 100644 src/operations/Progress.as create mode 100644 src/operations/ProgressOperationEvent.as create mode 100644 src/operations/QueueProgress.as diff --git a/src/operations/CompoundOperation.as b/src/operations/CompoundOperation.as index b19ac31..55defe3 100644 --- a/src/operations/CompoundOperation.as +++ b/src/operations/CompoundOperation.as @@ -133,6 +133,7 @@ package operations _executingOperations.remove(operation); _finishedOperationsCount++; + progressed(_finishedOperationsCount); // check to see if all the operations are finished. if (isExecuting && _finishedOperationsCount == _operations.length) { @@ -198,5 +199,13 @@ package operations { return _operations; } + + /** + * @inheritDoc + */ + override protected function get unitsTotal():Number + { + return operationSet.length; + } } } \ No newline at end of file diff --git a/src/operations/Operation.as b/src/operations/Operation.as index 2844170..5ee36a8 100644 --- a/src/operations/Operation.as +++ b/src/operations/Operation.as @@ -18,6 +18,11 @@ package operations */ [Event(name="beforeExecute", type="operations.OperationEvent")] + /** + * Dispatched when the operation has progressed during its execution. + */ + [Event(name="progress", type="operations.ProgressOperationEvent")] + /** * Dispatched when either an error or fault has occurred during the execution * of an operation. @@ -46,6 +51,12 @@ package operations */ public class Operation extends EventDispatcher { + private static const QUEUED:int = 0; + private static const EXECUTING:int = 1; + private static const FINISHED:int = 2; + + private var _state:int = QUEUED; + /** * Constructor. */ @@ -80,8 +91,8 @@ package operations */ final public function execute():void { - if (!isExecuting) { - _isExecuting = true; + if (isQueued) { + _state = EXECUTING; fireBeforeExecute(); if (isExecuting) { @@ -150,6 +161,7 @@ package operations { if (isExecuting) { fireFault(summary, detail); + _hasErrored = true; finish(false); } } @@ -166,7 +178,8 @@ package operations final protected function finish(successful:Boolean):void { if (isExecuting) { - _isExecuting = false; + _state = FINISHED; + progressed(unitsTotal); fireFinished(successful); } } @@ -192,6 +205,13 @@ package operations } } + private function fireProgress():void + { + if (hasEventListener(ProgressOperationEvent.PROGRESS)) { + dispatchEvent( new ProgressOperationEvent(ProgressOperationEvent.PROGRESS) ); + } + } + private function fireFault(summary:String, detail:String = ""):void { if (hasEventListener(FaultOperationEvent.FAULT)) { @@ -226,6 +246,32 @@ package operations return data; } + /** + * Called by sub-classes to indicate the progress of the operation. + * + * @param unitsComplete The number of units completed. + */ + final protected function progressed(unitsComplete:Number):void + { + _progress.complete = unitsComplete; + fireProgress(); + } + + /** + * Resets the operation to its queued state. If the operation is executing, it will be + * canceled. If the operation has finished its errors, result data, and progress will + * be reset. + */ + final public function reset():void + { + if (isExecuting) { + cancel(); + } + progress.complete = 0; + _hasErrored = false; + _state = QUEUED; + } + /** * Called by a sub-class to indicate that a result was received during the * execution of the operation. @@ -302,13 +348,66 @@ package operations return classNameParts.length > 1 ? classNameParts[1] : classNameParts[0]; } - private var _isExecuting:Boolean; /** * Indicates whether the request is currently executing. */ final public function get isExecuting():Boolean { - return _isExecuting; + return _state == EXECUTING; + } + + /** + * Indicates whether the request is finished, either successfully or unsuccessfully. + */ + final public function get isFinished():Boolean + { + return _state == FINISHED; + } + + /** + * Indicates whether the request is idle and ready to be executed. + */ + final public function get isQueued():Boolean + { + return _state == QUEUED; + } + + /** + * Indicates whether the request has finished, and hasn't errored. + */ + final public function get isSuccessful():Boolean + { + return isFinished && !hasErrored; + } + + private var _hasErrored:Boolean; + /** + * true if this operation errored during its execution. + */ + final public function get hasErrored():Boolean + { + return _hasErrored; + } + + private var _progress:Progress; + /** + * Information about the progress of this operation. + */ + public function get progress():Progress + { + if (_progress == null) { + _progress = new Progress(); + _progress.total = unitsTotal; + } + return _progress; + } + + /** + * The number of units needed to complete this operation. + */ + protected function get unitsTotal():Number + { + return 1; } } } \ No newline at end of file diff --git a/src/operations/OperationQueue.as b/src/operations/OperationQueue.as new file mode 100644 index 0000000..d1df221 --- /dev/null +++ b/src/operations/OperationQueue.as @@ -0,0 +1,259 @@ +package operations +{ + import flash.events.EventDispatcher; + + import mx.collections.ArrayList; + import mx.collections.IList; + import mx.collections.ListCollectionView; + + /** + * An OperationQueue manages the execution of a set of Operations. + * This class allows you to queue operations that are computationally expensive, or take large + * amounts of time, such as uploading. + * + *

+ * Operations are queued by passing in the operation to queue(). This method adds + * the operation to the end of the queue. Once started, the queue will execute each operation + * until the queue is empty. Multiple operations can be executed simultaneously by passing in a + * simultaneousCount to the queue's constructor. Operations can be added and removed + * from the queue while it's running. + *

+ * + *

+ * Users can monitor the progress of the queue by using the queue's progress + * property. This object contains how many units of work that the queue has completed, and how + * many units of work total are needed to complete the whole queue. + *

+ * + *

+ * The queue also maintains bindable lists that your views can use to display which operations + * are queued, which operations have finished, and which operations have errored. You can pass + * these lists to the data provider of any Spark or MX list control. + *

+ * + * @author Dan Schultz + */ + public class OperationQueue extends EventDispatcher + { + private var _simultaneousCount:int; + + /** + * Constructor. + */ + public function OperationQueue(simultaneousCount:int = 1) + { + super(); + _simultaneousCount = simultaneousCount; + } + + /** + * Adds an operation to the end of the queue to be executed. If the operation is + * executing, it will be requeued. + * + * @param operation The operation to queue. + */ + public function queue(operation:Operation):void + { + remove(operation); + operation.reset(); + items.addItem(operation); + progress.total += operation.progress.total; + + executeAvailable(); + } + + /** + * Removes an operation from the queue. If the operation is executing, it will be + * canceled. + * + * @param operation The operation to remove. + */ + public function remove(operation:Operation):void + { + if (_items.removeItem(operation)) { + if (operation.isFinished) { + progress.complete -= operation.progress.complete; + } + progress.total -= operation.progress.total; + } + } + + /** + * Removes all operation from the queue. Any operations that are executing will be + * canceled. + */ + public function clear():void + { + while (items.length > 0) { + remove(Operation( items.getItemAt(0) )); + } + } + + /** + * Starts the queue. + */ + public function start():void + { + if (!isRunning) { + _isRunning = true; + executeAvailable(); + } + } + + /** + * Pauses the queue. + */ + public function pause():void + { + if (isRunning) { + _isRunning = false; + + while (executing.length > 0) { + (executing.getItemAt(0) as Operation).reset(); + } + } + } + + private function executeAvailable():void + { + while (executing.length < _simultaneousCount) { + execute(next()); + } + } + + private function execute(operation:Operation):void + { + if (!operation.isQueued) { + throw new ArgumentError("Attempted to execute non-queued operation."); + } + + operation.addEventListener(ProgressOperationEvent.PROGRESS, handleOperationProgress); + operation.addEventListener(FinishedOperationEvent.FINISHED, handleOperationFinished); + operation.execute(); + } + + private function handleOperationProgress(event:FinishedOperationEvent):void + { + + } + + private function handleOperationFinished(event:FinishedOperationEvent):void + { + if (event.successful) { + progress.complete += event.operation.progress.complete; + } + + event.operation.removeEventListener(ProgressOperationEvent.PROGRESS, handleOperationProgress); + event.operation.removeEventListener(FinishedOperationEvent.FINISHED, handleOperationFinished); + executeAvailable(); + } + + /** + * The next operation in the queue to be executed. + * + * @return A queued operation, or null if the queue is empty. + */ + public function next():Operation + { + return Operation( queued().getItemAt(0) ); + } + + private var _isRunning:Boolean; + /** + * true if the queue is currently running. + */ + public function get isRunning():Boolean + { + return _isRunning; + } + + private var _errored:ListCollectionView; + /** + * The set of Operations that have executed and errored. + */ + public function errored():IList + { + if (_errored == null) { + _errored = new ListCollectionView(items); + _errored.filterFunction = function(operation:Operation):Boolean + { + return operation.hasErrored; + }; + _errored.refresh(); + } + return _errored; + } + + private var _executing:ListCollectionView; + /** + * The set of Operations that are currently executing. + */ + public function get executing():IList + { + if (_executing == null) { + _executing = new ListCollectionView(items); + _executing.filterFunction = function(operation:Operation):Boolean + { + return operation.isExecuting; + }; + _executing.refresh(); + } + return _executing; + } + + private var _queued:ListCollectionView; + /** + * The set of Operations that are awaiting to be executed. + */ + public function queued():IList + { + if (_queued == null) { + _queued = new ListCollectionView(items); + _queued.filterFunction = function(operation:Operation):Boolean + { + return operation.isQueued; + }; + _queued.refresh(); + } + return _queued; + } + + private var _successful:ListCollectionView; + /** + * The set of Operations have finished successfully. + */ + public function get successful():IList + { + if (_successful == null) { + _successful = new ListCollectionView(items); + _successful.filterFunction = function(operation:Operation):Boolean + { + return operation.isSuccessful; + }; + _successful.refresh(); + } + return _successful; + } + + private var _items:ArrayList = new ArrayList(); + /** + * The set of all operations to execute. + */ + public function get items():IList + { + return _items; + } + + private var _progress:QueueProgress; + /** + * The progress of the queue. + */ + public function get progress():Progress + { + if (_progress == null) { + _progress = new QueueProgress(this); + } + return _progress; + } + } +} \ No newline at end of file diff --git a/src/operations/Progress.as b/src/operations/Progress.as new file mode 100644 index 0000000..cc2b0a9 --- /dev/null +++ b/src/operations/Progress.as @@ -0,0 +1,59 @@ +package operations +{ + /** + * The Progress class contains information about the progress of an + * operation. It holds the number of units that have completed, and the total number + * of units needed to complete the operation. + * + * @author Dan Schultz + */ + public class Progress + { + /** + * Constructor. + * + * @param total The total units needed to complete. + */ + public function Progress() + { + + } + + /** + * Returns the percentage as a string. + * + * @return A string. + */ + public function toString():String + { + var percentage:Number = complete/total; + return percentage.toString() + (!isNaN(percentage) ? "%" : ""); + } + + private var _complete:Number = 0; + /** + * The number of units that have completed. + */ + public function get complete():Number + { + return _complete; + } + public function set complete(value:Number):void + { + _complete = value; + } + + private var _total:Number = 0; + /** + * The total number of units to complete the operation. + */ + public function get total():Number + { + return _total; + } + public function set total(value:Number):void + { + _total = value; + } + } +} \ No newline at end of file diff --git a/src/operations/ProgressOperationEvent.as b/src/operations/ProgressOperationEvent.as new file mode 100644 index 0000000..12c665c --- /dev/null +++ b/src/operations/ProgressOperationEvent.as @@ -0,0 +1,41 @@ +package operations +{ + import flash.events.ProgressEvent; + + /** + * An event that is dispatched by an operation to indicate its progress. + * + * @author Dan Schultz + */ + public class ProgressOperationEvent extends OperationEvent + { + /** + * An event type for when an operation has progressed. + */ + public static const PROGRESS:String = "progress"; + + /** + * @copy OperationEvent#OperationEvent() + */ + public function ProgressOperationEvent(type:String) + { + super(type); + } + + /** + * @copy Operation#unitsComplete + */ + public function get unitsComplete():Number + { + return operation.progress.complete; + } + + /** + * @copy Operation#unitsTotal + */ + public function get unitsTotal():Number + { + return operation.progress.total; + } + } +} \ No newline at end of file diff --git a/src/operations/QueueProgress.as b/src/operations/QueueProgress.as new file mode 100644 index 0000000..d99765a --- /dev/null +++ b/src/operations/QueueProgress.as @@ -0,0 +1,36 @@ +package operations +{ + import mesh.core.number.Fraction; + + /** + * A progress class that's specific for an OperationQueue. + * + * @author Dan Schultz + */ + public class QueueProgress extends Progress + { + private var _queue:OperationQueue; + + /** + * Constructor. + * + * @param queue The queue that owns this progress. + */ + public function QueueProgress(queue:OperationQueue) + { + _queue = queue; + } + + /** + * @inheritDoc + */ + override public function get complete():Number + { + var temp:Number = 0; + for each (var executing:Operation in _queue.executing) { + temp += executing.progress.complete; + } + return super.complete + temp; + } + } +} \ No newline at end of file