@mezzy/commands
Version:
A luxurious user experience framework, developed by your friends at Mezzanine.
291 lines (226 loc) • 9.71 kB
text/typescript
import is from '@mezzy/is';
import Debug from '@mezzy/debug';
import { IPromise, Result } from '@mezzy/result';
import { Signal } from '@mezzy/signals';
import { Identifier } from '@mezzy/ids';
import { MapList } from '@mezzy/collections';
import { Queue } from '@mezzy/collections';
import Command from './command';
import CommandEventArgs from './commandEventArgs';
Debug.setCategoryMode('commands', false);
export class CommandManager {
constructor(isUndoEnabled:boolean = true) { this._isUndoEnabled = isUndoEnabled; }
/*====================================================================*
START: Properties
*====================================================================*/
/**
* A normalized value representing progress while running queued commands.
* The value starts at 0 when run() is initially called, and progresses to 1
* once all the queued commands have been run and the command queue is empty.
*/
get progress():number { return this._progress; }
private _progress:number = 0;
/**
* The command queue that holds any pending Commands that are waiting to be run.
*/
get pending():number { return this._pendingQueue.size; }
//===== Private
private _isUndoEnabled:boolean = true;
private _commands:MapList<string, Command> = new MapList<string, Command>();
private _pendingQueue:Queue<Command> = new Queue<Command>();
private _undoQueue:Queue<Command> = new Queue<Command>();
private _redoQueue:Queue<Command> = new Queue<Command>();
private _isRunningAll:boolean = false;
get runStartCount():number { return this._runStartCount; }
private _runStartCount:number = 0;
get lastReturnValue():any { return this._lastReturnValue; }
private _lastReturnValue:any;
/*====================================================================*
START: Signals
*====================================================================*/
/**
* Dispatched immediately before each command is run.
*/
commandRunStart:Signal<CommandEventArgs> = new Signal<CommandEventArgs>();
/**
* Dispatched immediately after each command has completed running.
*/
commandRunComplete:Signal<CommandEventArgs> = new Signal<CommandEventArgs>();
/*====================================================================*
START: Methods
*====================================================================*/
/***
* Register a command with the command manager that can later be retrieved by key.
* If null or empty string is passed for key, a guid is generated for the ICommand key and returned as a string.
*/
register(
scope:Object,
runFunction:(...args:any[]) => any,
undoFunction?:(...args:any[]) => any,
name?:string
):string {
if (is.empty(runFunction)) return;
let command:Command = new Command(runFunction, undoFunction, scope);
if (is.empty(name)) name = 'command_' + Identifier.getSessionUniqueInteger();
this._commands.set(name, command);
return name;
}
unregister(name:string):void { this._commands.delete(name); }
/**
* Retrieve a stored command by key.
*/
get(name:string):Command {
let command:Command = this._commands.get(name);
if (is.empty(command)) {
command = new Command();
this._commands.set(name, command);
return command;
} else return command;
}
/**
* Add a command to the command queue. Once all commands have been added to the queue,
* call run() to run them all in sequence.
*/
queue(command:Command, runArgs?:any[], undoArgs?:any[]):CommandManager {
if (is.empty(command)) return;
if (is.notEmpty(runArgs)) command.runArgs = runArgs;
if (is.notEmpty(undoArgs)) command.undoArgs = undoArgs;
this._pendingQueue.add(command);
return this;
}
/**
* Add the provided command (if any) to the queue with the specified arguments, then run all queued commands.
* The returned result completes with the return value of the last command to be run from the queue.
*/
run(command?:(Command | string), args?:any[], undoArgs?:any[]):IPromise<any> {
if (is.empty(command)) return this._runAll();
else {
let commandFinal:Command;
if (typeof command === 'string') commandFinal = this.get(command);
else commandFinal = <Command>command;
if (is.empty(commandFinal)) return Result.resolve(null);
this.queue(commandFinal, args, undoArgs);
return this._runAll();
}
}
/**
* Undo the last command that was run.
*/
undo(...args:any[]):IPromise<any> {
if (!this._isUndoEnabled || this._undoQueue.size < 1) return Result.resolve(null);
let command:Command = this._undoQueue.deleteLast();
if (is.empty(command)) return Result.resolve(null);
let result:Result<any> = command.undo.apply(command, args);
result.then((returnValue:any) => {
this._redoQueue.add(command);
});
return result;
}
/**
* Re-run the last command that was called from the undo queue.
*/
redo(...args:any[]):IPromise<any> {
if (!this._isUndoEnabled || this._redoQueue.size < 1) return Result.resolve(null);
let command:Command = this._redoQueue.deleteLast();
if (is.empty(command)) return Result.resolve(null);
let result:Result<any> = command.run.apply(command, args);
result.then((returnValue:any) => {
let commandEventArgs:CommandEventArgs = new CommandEventArgs(command, 1, returnValue, args);
commandEventArgs.isCommandComplete = true;
this.commandRunComplete.dispatch(commandEventArgs);
this._undoQueue.add(command);
});
return result;
}
/**
* Clear the pending, undo and redo queues.
*/
clearAllQueues():CommandManager {
this._pendingQueue.clear();
this._undoQueue.clear();
this._redoQueue.clear();
return this;
}
/**
* Clear the pending queue.
*/
clearPending():CommandManager {
this._pendingQueue.clear();
return this;
}
//===== Private
private _run(command:Command):IPromise<any> {
let pending:number = this._pendingQueue.size + 1;
let progressStart:number = 1 - (pending / this._runStartCount);
let argsStart:CommandEventArgs = new CommandEventArgs(command, progressStart);
if (this._isRunningAll) {
argsStart.pending = this._pendingQueue.size + 1;
argsStart.totalRunning = this._runStartCount;
}
this.commandRunStart.dispatch(argsStart);
if (this._isUndoEnabled) {
this._undoQueue.add(command);
this._redoQueue.clear();
}
let result:IPromise<any> = command.run();
result.then((returnValue:any) => {
this._lastReturnValue = returnValue;
let progressComplete:number = 1 - ((pending - 1) / this._runStartCount);
let argsComplete:CommandEventArgs = new CommandEventArgs(command, progressComplete, returnValue);
if (this._isRunningAll) {
argsComplete.pending = this._pendingQueue.size;
argsComplete.totalRunning = this._runStartCount;
}
argsComplete.isCommandComplete = true;
this.commandRunComplete.dispatch(argsComplete);
});
return result;
}
private _runNext():IPromise<any> {
if (this._pendingQueue.size > 0) {
let command:Command = this._pendingQueue.deleteFirst();
if (is.empty(command)) {
Debug.error('Expected commands on pending stack, but found none.', 'CommandManager.runNext(...)');
return Result.resolve<any>(null);
}
Debug.log(
command.message + (is.empty(command.message) ? '' : ' ')
+ 'p = ' + this._progress.toString() + ', remaining: ' + this._pendingQueue.size.toString()
+ ', total: ' + this._runStartCount.toString(),
null, 'commands'
);
return this._run(command);
} else return Result.resolve<any>(null);
}
/**
* The returned result completes with the return value of the last command to be run from the queue.
*/
private _runAll():IPromise<any> {
if (!this._isRunningAll) {
this._isRunningAll = true;
this._progress = 0;
this._resultRunAll = new Result<any>();
this._lastReturnValue = null;
this._runStartCount = this._pendingQueue.size;
if (this._runStartCount > 0) this._runAllNext();
else this._resultRunAll.resolve(null);
}
return this._resultRunAll;
}
private _resultRunAll:Result<any>;
private _runAllNext():void {
if (this._pendingQueue.size > 0) {
this._runNext().then((result:any) => {
this._progress = 1 - (this._pendingQueue.size / this._runStartCount);
this._runAllNext();
});
} else {
this._isRunningAll = false;
this._progress = 1;
/// All pending commands have been run in the current batch.
/// So, complete with the result of the last command run.
this._resultRunAll.resolve(this._lastReturnValue);
}
}
} // End class
export default CommandManager;