/**
* Represents a read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}.
* Operation objects are used to enable communication between Stores and Proxies.
* Application developers should rarely need to interact with Operation objects directly.
*
* Several Operations can be batched together in a {@link Ext.data.Batch batch}.
*/
Ext.define('Ext.data.operation.Operation', {
alternateClassName: 'Ext.data.Operation',
isOperation: true,
config: {
/**
* @cfg {Boolean} synchronous
* True if this Operation is to be executed synchronously. This property is inspected by a
* {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel
* or not.
*/
synchronous: false,
/**
* @cfg {String} url
* The url for this operation. Typically this will be provided by a proxy and not configured
* here.
*/
url: '',
/**
* @cfg {Object} params
* Parameters to pass along with the request when performing the operation.
*/
params: undefined,
/**
* @cfg {Function} callback
* Function to execute when operation completed.
* @cfg {Ext.data.Model[]} callback.records Array of records.
* @cfg {Ext.data.operation.Operation} callback.operation The Operation itself.
* @cfg {Boolean} callback.success True when operation completed successfully.
*/
callback: undefined,
/**
* @cfg {Object} scope
* Scope for the {@link #callback} function.
*/
scope: undefined,
/**
* @cfg {Ext.data.ResultSet} resultSet
* The ResultSet for this operation.
* @accessor
*/
resultSet: null,
/**
* @private
* @cfg {Object} response
* The response for this operation.
*/
response: null,
/**
* @cfg {Ext.data.Request} request
* The request for this operation.
*/
request: null,
/**
* @cfg {Ext.data.Model[]} records
* The records associated with this operation. If this is a `read` operation, this will be
* `null` until data is returned from the {@link Ext.data.proxy.Proxy}.
*/
records: null,
/**
* @cfg {Object} id
* The id of the operation.
*/
id: undefined,
/**
* @cfg {Ext.data.proxy.Proxy} proxy
* The proxy for this operation
*/
proxy: null,
/**
* @cfg {Ext.data.Batch}
* The batch for this operation, if applicable
*/
batch: null,
/**
* @cfg {Function} recordCreator
* Passed to the reader, see {@link Ext.data.reader.Reader#read}
* @private
*/
recordCreator: null,
// We use this because in a lot of cases the developer can indirectly pass
// a callback/scope and that will get pushed on to the operation. As such,
// create our own hook for the callback that will fire first
/**
* @cfg {Function} internalCallback
* A callback to run before the {@link #callback}.
* @private
*/
internalCallback: null,
/**
* @cfg {Object} internalScope
* Scope to run the {@link #internalCallback}
* @private
*/
internalScope: null
},
/**
* @property {Number} order
* This number is used by `{@link Ext.data.Batch#sort}` to order operations. Order of
* operations of the same type is determined by foreign-key dependencies. The array of
* operations is sorted by ascending (increasing) values of `order`.
* @private
* @readonly
* @since 5.0.0
*/
order: 0,
/**
* @property {Number} foreignKeyDirection
* This number is used by `{@link Ext.data.Batch#sort}` to order operations of the
* same type. This value is multipled by the "entity rank" (which is determined by
* foreign-key dependencies) to invert the direction of the sort based on the type of
* operation. Only `Ext.data.operation.Destroy` overrides this value.
* @private
* @readonly
* @since 5.0.0
*/
foreignKeyDirection: 1,
/**
* @property {Boolean} started
* The start status of this Operation. Use {@link #isStarted}.
* @readonly
* @private
*/
started: false,
/**
* @property {Boolean} running
* The run status of this Operation. Use {@link #isRunning}.
* @readonly
* @private
*/
running: false,
/**
* @property {Boolean} complete
* The completion status of this Operation. Use {@link #isComplete}.
* @readonly
* @private
*/
complete: false,
/**
* @property {Boolean} success
* Whether the Operation was successful or not. This starts as undefined and is set to true
* or false by the Proxy that is executing the Operation. It is also set to false by
* {@link #setException}. Use {@link #wasSuccessful} to query success status.
* @readonly
* @private
*/
success: undefined,
/**
* @property {Boolean} exception
* The exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
* @readonly
* @private
*/
exception: false,
/**
* @property {String/Object} error
* The error object passed when {@link #setException} was called. This could be any object or
* primitive.
* @private
*/
error: undefined,
idPrefix: 'ext-operation-',
/**
* Creates new Operation object.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
// This ugliness is here to prevent an issue when specifying scope
// as an object literal. The object will be pulled in as part of
// the merge() during initConfig which will change the object
// reference. As such, we attempt to fudge it here until we come
// up with a better solution for describing how specific config
// objects should behave during init() time.
var scope = config && config.scope;
this.initConfig(config);
if (config) {
config.scope = scope;
}
if (scope) {
this.setScope(scope);
this.initialConfig.scope = scope;
}
// We need an internal id to track operations in Proxy
this._internalId = Ext.id(this, this.idPrefix);
},
getAction: function() {
return this.action;
},
/**
* @private
* Executes the operation on the configured {@link #proxy}.
* @return {Ext.data.Request} The request object
*/
execute: function() {
var me = this,
request;
delete me.error;
delete me.success;
me.complete = me.exception = false;
me.setStarted();
me.request = request = me.doExecute();
if (request) {
request.setOperation(me);
}
return request;
},
doExecute: Ext.emptyFn,
/**
* Aborts the processing of this operation on the {@link #proxy}.
* This is only valid for proxies that make asynchronous requests.
*/
abort: function() {
var me = this,
request = me.request,
proxy;
me.aborted = true;
if (me.running && request) {
proxy = me.getProxy();
if (proxy && !proxy.destroyed) {
proxy.abort(request);
}
me.request = null;
}
me.running = false;
},
process: function(resultSet, request, response, autoComplete) {
var me = this;
autoComplete = autoComplete !== false;
me.setResponse(response);
me.setResultSet(resultSet);
if (resultSet.getSuccess()) {
me.doProcess(resultSet, request, response);
me.setSuccessful(autoComplete);
}
else if (autoComplete) {
me.setException(resultSet.getMessage());
}
},
/**
* @private
* This private object is used to save on memory allocation. This instance is used to
* apply server record updates as part of a record commit performed by calling the
* set() method on the record. See doProcess.
*/
_commitSetOptions: { convert: true, commit: true },
/**
* Process records in the operation after the response is successful and the result
* set is parsed correctly. The base class implementation of this method is used by
* "create" and "update" operations to allow the server response to update the client
* side records.
*
* @param {Ext.data.ResultSet} resultSet The result set
* @param {Ext.data.Request} request The request
* @param {Object} response The response
* @protected
*/
doProcess: function(resultSet, request, response) {
var me = this,
commitSetOptions = me._commitSetOptions,
clientRecords = me.getRecords(),
clientLen = clientRecords.length,
clientIdProperty = clientRecords[0].clientIdProperty,
serverRecords = resultSet.getRecords(), // a data array, not records yet
serverLen = serverRecords ? serverRecords.length : 0,
clientMap, serverRecord, clientRecord, i;
if (serverLen && clientIdProperty) {
// Linear pass over clientRecords to map them by their idProperty
clientMap = Ext.Array.toValueMap(clientRecords, 'id');
// Linear pass over serverRecords to match them by clientIdProperty to the
// corresponding clientRecord (if one exists).
for (i = 0; i < serverLen; ++i) {
serverRecord = serverRecords[i];
clientRecord = clientMap[serverRecord[clientIdProperty]];
if (clientRecord) {
// Remove this one so we don't commit() on it next
delete clientMap[clientRecord.id];
// Remove the clientIdProperty value since we don't want to store it
delete serverRecord[clientIdProperty];
clientRecord.set(serverRecord, commitSetOptions); // set & commit
}
//<debug>
else {
Ext.log.warn('Ignoring server record: ' + Ext.encode(serverRecord));
}
//</debug>
}
// Linear pass over any remaining client records.
for (i in clientMap) {
clientMap[i].commit();
}
}
else {
// Either no serverRecords or no clientIdProperty, so index correspondence is
// all we have to go on. If there is no serverRecord at a given index we just
// commit() the record.
for (i = 0; i < clientLen; ++i) {
clientRecord = clientRecords[i];
if (serverLen === 0 || !(serverRecord = serverRecords[i])) {
// once i > serverLen then serverRecords[i] will be undefined...
clientRecord.commit();
}
else {
clientRecord.set(serverRecord, commitSetOptions);
}
}
}
},
/**
* Marks the Operation as started.
*/
setStarted: function() {
this.started = this.running = true;
},
/**
* Marks the Operation as completed.
*/
setCompleted: function() {
var me = this,
proxy;
me.complete = true;
me.running = false;
if (!me.destroying) {
me.triggerCallbacks();
}
// Operation can be destroyed in callback
if (me.destroyed) {
return;
}
proxy = me.getProxy();
// Store and proxy could be destroyed in callbacks
if (proxy && !proxy.destroyed) {
proxy.completeOperation(me);
}
},
/**
* Marks the Operation as successful.
* @param {Boolean} [complete] `true` to also mark this operation
* as being complete See {@link #setCompleted}.
*/
setSuccessful: function(complete) {
this.success = true;
if (complete) {
this.setCompleted();
}
},
/**
* Marks the Operation as having experienced an exception. Can be supplied with an option
* error message/object.
* @param {String/Object} error (optional) error string/object
*/
setException: function(error) {
var me = this;
me.exception = true;
me.success = me.running = false;
me.error = error;
me.setCompleted();
},
triggerCallbacks: function() {
var me = this,
callback = me.getInternalCallback();
// Call internal callback first (usually the Store's onProxyLoad method)
if (callback) {
callback.call(me.getInternalScope() || me, me);
// Operation callback can cause it to be destroyed
if (me.destroyed) {
return;
}
me.setInternalCallback(null);
me.setInternalScope(null);
}
// Call the user's callback as passed to Store's read/write
callback = me.getCallback();
if (callback) {
// Maintain the public API for callback
callback.call(me.getScope() || me, me.getRecords(), me, me.wasSuccessful());
if (me.destroyed) {
return;
}
me.setCallback(null);
me.setScope(null);
}
},
/**
* Returns true if this Operation encountered an exception (see also {@link #getError})
* @return {Boolean} True if there was an exception
*/
hasException: function() {
return this.exception;
},
/**
* Returns the error string or object that was set using {@link #setException}
* @return {String/Object} The error object
*/
getError: function() {
return this.error;
},
/**
* Returns the {@link Ext.data.Model record}s associated with this operation. For read
* operations the records as set by the {@link Ext.data.proxy.Proxy Proxy} will be
* returned (returns `null` if the proxy has not yet set the records).
*
* For create, update, and destroy operations the operation's initially configured
* records will be returned, although the proxy may modify these records' data at some
* point after the operation is initialized.
*
* @return {Ext.data.Model[]}
*/
getRecords: function() {
var resultSet;
/* eslint-disable-next-line no-cond-assign */
return this._records || ((resultSet = this.getResultSet()) ? resultSet.getRecords() : null);
},
/**
* Returns true if the Operation has been started. Note that the Operation may have started
* AND completed, see {@link #isRunning} to test if the Operation is currently running.
* @return {Boolean} True if the Operation has started
*/
isStarted: function() {
return this.started;
},
/**
* Returns true if the Operation has been started but has not yet completed.
* @return {Boolean} True if the Operation is currently running
*/
isRunning: function() {
return this.running;
},
/**
* Returns true if the Operation has been completed
* @return {Boolean} True if the Operation is complete
*/
isComplete: function() {
return this.complete;
},
/**
* Returns true if the Operation has completed and was successful
* @return {Boolean} True if successful
*/
wasSuccessful: function() {
return this.isComplete() && this.success === true; // success can be undefined
},
/**
* Checks whether this operation should cause writing to occur.
* @return {Boolean} Whether the operation should cause a write to occur.
*/
allowWrite: function() {
return true;
},
destroy: function() {
var me = this;
me.destroying = true;
if (me.running) {
me.abort();
}
// Cleanup upon destruction can be turned off
me._params = me._callback = me._scope = me._resultSet = me._response = null;
me.request = me._request = me._records = me._proxy = me._batch = null;
me._recordCreator = me._internalCallback = me._internalScope = null;
me.callParent();
}
});