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.
237 lines
9.1 KiB
237 lines
9.1 KiB
/** |
|
* 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;
|
|
|