You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
9.1 KiB
238 lines
9.1 KiB
10 years ago
|
/**
|
||
|
* Copyright 2013-2015, Facebook, Inc.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* This source code is licensed under the BSD-style license found in the
|
||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||
|
*
|
||
|
* @providesModule Transaction
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var invariant = require("./invariant");
|
||
|
|
||
|
/**
|
||
|
* `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 run while it is already being run. 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>
|
||
|
*
|
||
|
* 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 `componentDidUpdate` 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 updates.
|
||
|
*
|
||
|
* 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;
|
||
|
}
|
||
|
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.
|
||
|
* @return Return value from `method`.
|
||
|
*/
|
||
|
perform: function(method, scope, a, b, c, d, e, f) {
|
||
|
("production" !== process.env.NODE_ENV ? invariant(
|
||
|
!this.isInTransaction(),
|
||
|
'Transaction.perform(...): Cannot initialize a transaction when there ' +
|
||
|
'is already an outstanding transaction.'
|
||
|
) : invariant(!this.isInTransaction()));
|
||
|
var errorThrown;
|
||
|
var ret;
|
||
|
try {
|
||
|
this._isInTransaction = true;
|
||
|
// Catching errors makes debugging more difficult, so we start with
|
||
|
// errorThrown set to true before setting it to false after calling
|
||
|
// close -- if it's still set to true in the finally block, it means
|
||
|
// one of these calls threw.
|
||
|
errorThrown = true;
|
||
|
this.initializeAll(0);
|
||
|
ret = method.call(scope, a, b, c, d, e, f);
|
||
|
errorThrown = false;
|
||
|
} finally {
|
||
|
try {
|
||
|
if (errorThrown) {
|
||
|
// If `method` throws, prefer to show that stack trace over any thrown
|
||
|
// by invoking `closeAll`.
|
||
|
try {
|
||
|
this.closeAll(0);
|
||
|
} catch (err) {
|
||
|
}
|
||
|
} else {
|
||
|
// Since `method` didn't throw, we don't want to silence the exception
|
||
|
// here.
|
||
|
this.closeAll(0);
|
||
|
}
|
||
|
} finally {
|
||
|
this._isInTransaction = false;
|
||
|
}
|
||
|
}
|
||
|
return ret;
|
||
|
},
|
||
|
|
||
|
initializeAll: function(startIndex) {
|
||
|
var transactionWrappers = this.transactionWrappers;
|
||
|
for (var i = startIndex; i < transactionWrappers.length; i++) {
|
||
|
var wrapper = transactionWrappers[i];
|
||
|
try {
|
||
|
// Catching errors makes debugging more difficult, so we start with the
|
||
|
// OBSERVED_ERROR state before overwriting it with the real return value
|
||
|
// of initialize -- if it's still set to OBSERVED_ERROR in the finally
|
||
|
// block, it means wrapper.initialize threw.
|
||
|
this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
|
||
|
this.wrapperInitData[i] = wrapper.initialize ?
|
||
|
wrapper.initialize.call(this) :
|
||
|
null;
|
||
|
} finally {
|
||
|
if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
|
||
|
// The initializer for wrapper i threw an error; initialize the
|
||
|
// remaining wrappers but silence any exceptions from them to ensure
|
||
|
// that the first error is the one to bubble up.
|
||
|
try {
|
||
|
this.initializeAll(i + 1);
|
||
|
} catch (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(startIndex) {
|
||
|
("production" !== process.env.NODE_ENV ? invariant(
|
||
|
this.isInTransaction(),
|
||
|
'Transaction.closeAll(): Cannot close transaction when none are open.'
|
||
|
) : invariant(this.isInTransaction()));
|
||
|
var transactionWrappers = this.transactionWrappers;
|
||
|
for (var i = startIndex; i < transactionWrappers.length; i++) {
|
||
|
var wrapper = transactionWrappers[i];
|
||
|
var initData = this.wrapperInitData[i];
|
||
|
var errorThrown;
|
||
|
try {
|
||
|
// Catching errors makes debugging more difficult, so we start with
|
||
|
// errorThrown set to true before setting it to false after calling
|
||
|
// close -- if it's still set to true in the finally block, it means
|
||
|
// wrapper.close threw.
|
||
|
errorThrown = true;
|
||
|
if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
|
||
|
wrapper.close.call(this, initData);
|
||
|
}
|
||
|
errorThrown = false;
|
||
|
} finally {
|
||
|
if (errorThrown) {
|
||
|
// The closer for wrapper i threw an error; close the remaining
|
||
|
// wrappers but silence any exceptions from them to ensure that the
|
||
|
// first error is the one to bubble up.
|
||
|
try {
|
||
|
this.closeAll(i + 1);
|
||
|
} catch (e) {
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.wrapperInitData.length = 0;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var Transaction = {
|
||
|
|
||
|
Mixin: Mixin,
|
||
|
|
||
|
/**
|
||
|
* Token to look for to determine if an error occured.
|
||
|
*/
|
||
|
OBSERVED_ERROR: {}
|
||
|
|
||
|
};
|
||
|
|
||
|
module.exports = Transaction;
|