/** * 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 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. * *
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * 
* * 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} 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} 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) { invariant( !this.isInTransaction(), 'Transaction.perform(...): Cannot initialize a transaction when there ' + 'is already an outstanding transaction.' ); var memberStart = Date.now(); 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 { var memberEnd = Date.now(); this.methodInvocationTime += (memberEnd - memberStart); 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); } this._isInTransaction = false; } return ret; }, initializeAll: function(startIndex) { var transactionWrappers = this.transactionWrappers; var wrapperInitTimes = this.timingMetrics.wrapperInitTimes; for (var i = startIndex; i < transactionWrappers.length; i++) { var initStart = Date.now(); 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 { var curInitTime = wrapperInitTimes[i]; var initEnd = Date.now(); wrapperInitTimes[i] = (curInitTime || 0) + (initEnd - initStart); 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) { invariant( this.isInTransaction(), 'Transaction.closeAll(): Cannot close transaction when none are open.' ); var transactionWrappers = this.transactionWrappers; var wrapperCloseTimes = this.timingMetrics.wrapperCloseTimes; for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; var closeStart = Date.now(); 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 { var closeEnd = Date.now(); var curCloseTime = wrapperCloseTimes[i]; wrapperCloseTimes[i] = (curCloseTime || 0) + (closeEnd - closeStart); 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;