Files
react/src/utils/Transaction.js
T
Paul O’Shannessy 75897c2dcd Initial public release
2013-05-29 12:54:02 -07:00

253 lines
9.3 KiB
JavaScript

/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule Transaction
*/
"use strict";
var throwIf = require('throwIf');
var DUAL_TRANSACTION = 'DUAL_TRANSACTION';
var MISSING_TRANSACTION = 'MISSING_TRANSACTION';
if (__DEV__) {
DUAL_TRANSACTION =
'Cannot initialize transaction when there is already an outstanding ' +
'transaction. Common causes of this are trying to render a component ' +
'when you are already rendering a component or attempting a state ' +
'transition while in a render function. Another possibility is that ' +
'you are rendering new content (or state transitioning) in a ' +
'componentDidRender callback. If this is not the case, please report the ' +
'issue immediately.';
MISSING_TRANSACTION =
'Cannot close transaction when there is none open.';
}
/**
* `Transaction` creates a black box that is able to wrap any method such that
* certain invariants are maintained before and after the method is invoked
* (Even if an exception is thrown while invoking the wrapped method). Whoever
* instantiates a transaction can provide enforcers of the invariants at
* creation time. The `Transaction` class itself will supply one additional
* automatic invariant for you - the invariant that any transaction instance
* should not be ran while it is already being ran. You would typically create a
* single instance of a `Transaction` for reuse multiple times, that potentially
* is used to wrap several different methods. Wrappers are extremely simple -
* they only require implementing two methods.
*
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
*
* Bonus:
* - Reports timing metrics by method name and wrapper index.
*
* Use cases:
* - Preserving the input selection ranges before/after reconciliation.
* Restoring selection even in the event of an unexpected error.
* - Deactivating events while rearranging the DOM, preventing blurs/focuses,
* while guaranteeing that afterwards, the event system is reactivated.
* - Flushing a queue of collected DOM mutations to the main UI thread after a
* reconciliation takes place in a worker thread.
* - Invoking any collected `componentDidRender` callbacks after rendering new
* content.
* - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
* to preserve the `scrollTop` (an automatic scroll aware DOM).
* - (Future use case): Layout calculations before and after DOM upates.
*
* Transactional plugin API:
* - A module that has an `initialize` method that returns any precomputation.
* - and a `close` method that accepts the precomputation. `close` is invoked
* when the wrapped process is completed, or has failed.
*
* @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
* that implement `initialize` and `close`.
* @return {Transaction} Single transaction for reuse in thread.
*
* @class Transaction
*/
var Mixin = {
/**
* Sets up this instance so that it is prepared for collecting metrics. Does
* so such that this setup method may be used on an instance that is already
* initialized, in a way that does not consume additional memory upon reuse.
* That can be useful if you decide to make your subclass of this mixin a
* "PooledClass".
*/
reinitializeTransaction: function() {
this.transactionWrappers = this.getTransactionWrappers();
if (!this.wrapperInitData) {
this.wrapperInitData = [];
} else {
this.wrapperInitData.length = 0;
}
if (!this.timingMetrics) {
this.timingMetrics = {};
}
this.timingMetrics.methodInvocationTime = 0;
if (!this.timingMetrics.wrapperInitTimes) {
this.timingMetrics.wrapperInitTimes = [];
} else {
this.timingMetrics.wrapperInitTimes.length = 0;
}
if (!this.timingMetrics.wrapperCloseTimes) {
this.timingMetrics.wrapperCloseTimes = [];
} else {
this.timingMetrics.wrapperCloseTimes.length = 0;
}
this._isInTransaction = false;
},
_isInTransaction: false,
/**
* @abstract
* @return {Array<TransactionWrapper>} Array of transaction wrappers.
*/
getTransactionWrappers: null,
isInTransaction: function() {
return !!this._isInTransaction;
},
/**
* Executes the function within a safety window. Use this for the top level
* methods that result in large amounts of computation/mutations that would
* need to be safety checked.
*
* @param {function} method Member of scope to call.
* @param {Object} scope Scope to invoke from.
* @param {Object?=} args... Arguments to pass to the method (optional).
* Helps prevent need to bind in many cases.
* @returns Return value from `method`.
*/
perform: function(method, scope, a, b, c, d, e, f) {
throwIf(this.isInTransaction(), DUAL_TRANSACTION);
var memberStart = Date.now();
var err = null;
var ret;
try {
this.initializeAll();
ret = method.call(scope, a, b, c, d, e, f);
} catch (ie_requires_catch) {
err = ie_requires_catch;
} finally {
var memberEnd = Date.now();
this.methodInvocationTime += (memberEnd - memberStart);
try {
this.closeAll();
} catch (closeAllErr) {
err = err || closeAllErr;
}
}
if (err) {
throw err;
}
return ret;
},
initializeAll: function() {
this._isInTransaction = true;
var transactionWrappers = this.transactionWrappers;
var wrapperInitTimes = this.timingMetrics.wrapperInitTimes;
var err = null;
for (var i = 0; i < transactionWrappers.length; i++) {
var initStart = Date.now();
var wrapper = transactionWrappers[i];
try {
this.wrapperInitData[i] =
wrapper.initialize ? wrapper.initialize.call(this) : null;
} catch (initErr) {
err = err || initErr; // Remember the first error.
this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
} finally {
var curInitTime = wrapperInitTimes[i];
var initEnd = Date.now();
wrapperInitTimes[i] = (curInitTime || 0) + (initEnd - initStart);
}
}
if (err) {
throw err;
}
},
/**
* Invokes each of `this.transactionWrappers.close[i]` functions, passing into
* them the respective return values of `this.transactionWrappers.init[i]`
* (`close`rs that correspond to initializers that failed will not be
* invoked).
*/
closeAll: function() {
throwIf(!this.isInTransaction(), MISSING_TRANSACTION);
var transactionWrappers = this.transactionWrappers;
var wrapperCloseTimes = this.timingMetrics.wrapperCloseTimes;
var err = null;
for (var i = 0; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
var closeStart = Date.now();
var initData = this.wrapperInitData[i];
try {
if (initData !== Transaction.OBSERVED_ERROR) {
wrapper.close && wrapper.close.call(this, initData);
}
} catch (closeErr) {
err = err || closeErr; // Remember the first error.
} finally {
var closeEnd = Date.now();
var curCloseTime = wrapperCloseTimes[i];
wrapperCloseTimes[i] = (curCloseTime || 0) + (closeEnd - closeStart);
}
}
this.wrapperInitData.length = 0;
this._isInTransaction = false;
if (err) {
throw err;
}
}
};
var Transaction = {
Mixin: Mixin,
/**
* Token to look for to determine if an error occured.
*/
OBSERVED_ERROR: {}
};
module.exports = Transaction;