227 lines
9.3 KiB
JavaScript
227 lines
9.3 KiB
JavaScript
/**
|
|
* Copyright 2013-present, 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.
|
|
*
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var _prodInvariant = require('./reactProdInvariant');
|
|
|
|
var invariant = require('fbjs/lib/invariant');
|
|
|
|
var OBSERVED_ERROR = {};
|
|
|
|
/**
|
|
* `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 TransactionImpl = {
|
|
/**
|
|
* 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.length = 0;
|
|
} else {
|
|
this.wrapperInitData = [];
|
|
}
|
|
this._isInTransaction = false;
|
|
},
|
|
|
|
_isInTransaction: false,
|
|
|
|
/**
|
|
* @abstract
|
|
* @return {Array<TransactionWrapper>} Array of transaction wrappers.
|
|
*/
|
|
getTransactionWrappers: null,
|
|
|
|
isInTransaction: function () {
|
|
return !!this._isInTransaction;
|
|
},
|
|
|
|
/* eslint-disable space-before-function-paren */
|
|
|
|
/**
|
|
* 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. The optional arguments helps prevent the need
|
|
* to bind in many cases.
|
|
*
|
|
* @param {function} method Member of scope to call.
|
|
* @param {Object} scope Scope to invoke from.
|
|
* @param {Object?=} a Argument to pass to the method.
|
|
* @param {Object?=} b Argument to pass to the method.
|
|
* @param {Object?=} c Argument to pass to the method.
|
|
* @param {Object?=} d Argument to pass to the method.
|
|
* @param {Object?=} e Argument to pass to the method.
|
|
* @param {Object?=} f Argument to pass to the method.
|
|
*
|
|
* @return {*} Return value from `method`.
|
|
*/
|
|
perform: function (method, scope, a, b, c, d, e, f) {
|
|
/* eslint-enable space-before-function-paren */
|
|
!!this.isInTransaction() ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Transaction.perform(...): Cannot initialize a transaction when there is already an outstanding transaction.') : _prodInvariant('27') : void 0;
|
|
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] = OBSERVED_ERROR;
|
|
this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
|
|
} finally {
|
|
if (this.wrapperInitData[i] === 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) {
|
|
!this.isInTransaction() ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : _prodInvariant('28') : void 0;
|
|
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 !== 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;
|
|
}
|
|
};
|
|
|
|
module.exports = TransactionImpl; |