/** * Provides a mechanism to run one or more {@link Ext.data.operation.Operation operations} * in a given order. Fires the `operationcomplete` event after the completion of each * Operation, and the `complete` event when all Operations have been successfully executed. * Fires an `exception` event if any of the Operations encounter an exception. * * Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes. */Ext.define('Ext.data.Batch', { mixins: { observable: 'Ext.mixin.Observable' }, config: { /** * @cfg {Boolean} pauseOnException * True to pause the execution of the batch if any operation encounters an exception * (defaults to false). If you set this to true you are responsible for implementing * the appropriate handling logic and restarting or discarding the batch as needed. * There are different ways you could do this, e.g. by handling the batch's * {@link #event-exception} event directly, or perhaps by overriding * {@link Ext.data.ProxyStore#onBatchException onBatchException} at the store level. * If you do pause and attempt to handle the exception you can call {@link #retry} to * process the same operation again. * * Note that {@link Ext.data.operation.Operation operations} are atomic, so any operations * that may have succeeded prior to an exception (and up until pausing the batch) will be * finalized at the server level and will not be automatically reversible. Any transactional * / rollback behavior that might be desired would have to be implemented at the application * level. Pausing on exception will likely be most beneficial when used in coordination with * such a scheme, where an exception might actually affect subsequent operations in the same * batch and so should be handled before continuing with the next operation. * * If you have not implemented transactional operation handling then this option should * typically be left to the default of false (e.g. process as many operations as possible, * and handle any exceptions asynchronously without holding up the rest of the batch). */ pauseOnException: false }, /** * @property {Number} current * The index of the current operation being executed. * @private */ current: -1, /** * @property {Number} total * The total number of operations in this batch. * @private */ total: 0, /** * @property {Boolean} running * True if the batch is currently running. * @private */ running: false, /** * @property {Boolean} complete * True if this batch has been executed completely. * @private */ complete: false, /** * @property {Boolean} exception * True if this batch has encountered an exception. This is cleared at the start of each * operation. * @private */ exception: false, /** * Creates new Batch object. * @param {Object} [config] Config object */ constructor: function(config) { var me = this; me.mixins.observable.constructor.call(me, config); /** * @event complete * Fired when all operations of this batch have been completed * @param {Ext.data.Batch} batch The batch object * @param {Object} operation The last operation that was executed */ /** * @event exception * Fired when a operation encountered an exception * @param {Ext.data.Batch} batch The batch object * @param {Object} operation The operation that encountered the exception */ /** * @event operationcomplete * Fired when each operation of the batch completes * @param {Ext.data.Batch} batch The batch object * @param {Object} operation The operation that just completed */ /** * Ordered array of operations that will be executed by this batch * @property {Ext.data.operation.Operation[]} operations * @private */ me.operations = []; /** * Ordered array of operations that raised an exception during the most recent * batch execution and did not successfully complete * @property {Ext.data.operation.Operation[]} exceptions */ me.exceptions = []; }, /** * Adds a new operation to this batch at the end of the {@link #operations} array * @param {Ext.data.operation.Operation/Ext.data.operation.Operation[]} operation * The {@link Ext.data.operation.Operation Operation} object or an array of operations. * @return {Ext.data.Batch} this */ add: function(operation) { var me = this, i, len; if (Ext.isArray(operation)) { for (i = 0, len = operation.length; i < len; ++i) { me.add(operation[i]); } } else { me.total++; operation.setBatch(me); me.operations.push(operation); } return me; }, /** * Sorts the `{@link Ext.data.operation.Operation operations}` based on their type and * the foreign key dependencies of the entities. Consider a simple Parent and Child * case where the Child has a "parentId" field. If this batch contains two `create` * operations, one of a Parent and one for its Child, the server must receive and * process the `create` of the Parent before the Child can be created. * * In the case of `destroy` operations this order is reversed. The Child entity must be * destroyed before the Parent to avoid any foreign key constraints (a Child with an * invalid parentId field). * * Further, `create` operations must all occur before `update` operations to ensure * that all entities exist that might be now referenced by the updates. The created * entities can safely reference already existing entities. * * Finally, `destroy` operations are sorted after `update` operations to allow those * updates to remove references to the soon-to-be-deleted entities. */ sort: function() { this.operations.sort(this.sortFn); }, sortFn: function(operation1, operation2) { var ret = operation1.order - operation2.order; if (ret) { return ret; } /* eslint-disable-next-line vars-on-top, one-var */ var entityType1 = operation1.entityType, entityType2 = operation2.entityType, rank; // Since the orders are equal, the operations are the same type. Read operations // have no records, so report equality. if (!entityType1 || !entityType2) { return 0; } // Otherwise, determine the entity rank for the entities involved in the two // operations. if (!(rank = entityType1.rank)) { // Time to perform the topo-sort based on foreign-key references. entityType1.schema.rankEntities(); // Now the rank is available for all entities. rank = entityType1.rank; } return (rank - entityType2.rank) * operation1.foreignKeyDirection; }, /** * Kicks off execution of the batch, continuing from the next operation if the previous * operation encountered an exception, or if execution was paused. Use this method to start * the batch for the first time or to restart a paused batch by skipping the current * unsuccessful operation. * * To retry processing the current operation before continuing to the rest of the batch (e.g. * because you explicitly handled the operation's exception), call {@link #retry} instead. * * Note that if the batch is already running any call to start will be ignored. * @param {Number} [index] (private) * @return {Ext.data.Batch} this */ start: function(index) { var me = this; if (me.destroyed || !me.operations.length || me.running) { return me; } me.exceptions.length = 0; me.exception = false; me.running = true; return me.runOperation(Ext.isDefined(index) ? index : me.current + 1); }, abort: function() { var me = this, op; if (me.running) { op = me.getCurrent(); if (!op.destroyed) { op.abort(); } } me.running = false; me.aborted = true; me.current = undefined; }, /** * Kicks off execution of the batch, continuing from the current operation. This is intended * for restarting a {@link #pause paused} batch after an exception, and the operation that * raised the exception will now be retried. The batch will then continue with its normal * processing until all operations are complete or another exception is encountered. * * Note that if the batch is already running any call to retry will be ignored. * * @return {Ext.data.Batch} this */ retry: function() { return this.start(this.current); }, /** * @private * Runs the next operation, relative to this.current. * @return {Ext.data.Batch} this */ runNextOperation: function() { var me = this; if (me.running) { me.runOperation(me.current + 1); } return me; }, /** * Pauses execution of the batch, but does not cancel the current operation * @return {Ext.data.Batch} this */ pause: function() { this.running = false; return this; }, /** * Gets the operations for this batch. * @return {Ext.data.operation.Operation[]} The operations. */ getOperations: function() { return this.operations; }, /** * Gets any operations that have returned without success in this batch. * @return {Ext.data.operation.Operation[]} The exceptions */ getExceptions: function() { return this.exceptions; }, /** * Gets the currently running operation. Will return null if the batch has * not started or is completed. * @return {Ext.data.operation.Operation} The operation */ getCurrent: function() { var out = null, current = this.current; if (!(current === -1 || this.complete)) { out = this.operations[current]; } return out; }, /** * Gets the total number of operations in this batch. * @return {Number} The total */ getTotal: function() { return this.total; }, /** * Checks if this batch is running. * @return {Boolean} `true` if this batch is running. */ isRunning: function() { return this.running; }, /** * Checks if this batch is complete. * @return {Boolean} `true` if this batch is complete. */ isComplete: function() { return this.complete; }, /** * Checks if this batch has any exceptions. * @return {Boolean} `true` if this batch has any exceptions. */ hasException: function() { return this.exception; }, /** * Executes an operation by its numeric index in the {@link #operations} array * @param {Number} index The operation index to run * @return {Ext.data.Batch} this * * @private */ runOperation: function(index) { var me = this, operations = me.operations, operation = operations[index]; if (operation === undefined) { me.running = false; me.complete = true; me.fireEvent('complete', me, operations[operations.length - 1]); } else { me.current = index; operation.setInternalCallback(me.onOperationComplete); operation.setInternalScope(me); operation.execute(); } return me; }, onOperationComplete: function(operation) { var me = this, exception = operation.hasException(); if (exception) { me.exception = true; me.exceptions.push(operation); me.fireEvent('exception', me, operation); } if (exception && me.getPauseOnException()) { me.pause(); } else { me.fireEvent('operationcomplete', me, operation); me.runNextOperation(); } }, destroy: function() { var me = this, operations = me.operations, op, i, len; if (me.running) { me.abort(); } for (i = 0, len = me.operations.length; i < len; i++) { op = operations[i]; if (op) { if (!op.destroyed && !op.$destroyOwner) { op.destroy(); } op[i] = null; } } // Global cleanup can be turned off me.operations = me.exceptions = null; me.callParent(); }});