UNPKG

9.55 kBJavaScriptView Raw
1/**
2 * Copyright 2013-present, Facebook, Inc.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
8 *
9 *
10 */
11
12'use strict';
13
14var _prodInvariant = require('./reactProdInvariant');
15
16var invariant = require('fbjs/lib/invariant');
17
18var OBSERVED_ERROR = {};
19
20/**
21 * `Transaction` creates a black box that is able to wrap any method such that
22 * certain invariants are maintained before and after the method is invoked
23 * (Even if an exception is thrown while invoking the wrapped method). Whoever
24 * instantiates a transaction can provide enforcers of the invariants at
25 * creation time. The `Transaction` class itself will supply one additional
26 * automatic invariant for you - the invariant that any transaction instance
27 * should not be run while it is already being run. You would typically create a
28 * single instance of a `Transaction` for reuse multiple times, that potentially
29 * is used to wrap several different methods. Wrappers are extremely simple -
30 * they only require implementing two methods.
31 *
32 * <pre>
33 * wrappers (injected at creation time)
34 * + +
35 * | |
36 * +-----------------|--------|--------------+
37 * | v | |
38 * | +---------------+ | |
39 * | +--| wrapper1 |---|----+ |
40 * | | +---------------+ v | |
41 * | | +-------------+ | |
42 * | | +----| wrapper2 |--------+ |
43 * | | | +-------------+ | | |
44 * | | | | | |
45 * | v v v v | wrapper
46 * | +---+ +---+ +---------+ +---+ +---+ | invariants
47 * perform(anyMethod) | | | | | | | | | | | | maintained
48 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
49 * | | | | | | | | | | | |
50 * | | | | | | | | | | | |
51 * | | | | | | | | | | | |
52 * | +---+ +---+ +---------+ +---+ +---+ |
53 * | initialize close |
54 * +-----------------------------------------+
55 * </pre>
56 *
57 * Use cases:
58 * - Preserving the input selection ranges before/after reconciliation.
59 * Restoring selection even in the event of an unexpected error.
60 * - Deactivating events while rearranging the DOM, preventing blurs/focuses,
61 * while guaranteeing that afterwards, the event system is reactivated.
62 * - Flushing a queue of collected DOM mutations to the main UI thread after a
63 * reconciliation takes place in a worker thread.
64 * - Invoking any collected `componentDidUpdate` callbacks after rendering new
65 * content.
66 * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
67 * to preserve the `scrollTop` (an automatic scroll aware DOM).
68 * - (Future use case): Layout calculations before and after DOM updates.
69 *
70 * Transactional plugin API:
71 * - A module that has an `initialize` method that returns any precomputation.
72 * - and a `close` method that accepts the precomputation. `close` is invoked
73 * when the wrapped process is completed, or has failed.
74 *
75 * @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
76 * that implement `initialize` and `close`.
77 * @return {Transaction} Single transaction for reuse in thread.
78 *
79 * @class Transaction
80 */
81var TransactionImpl = {
82 /**
83 * Sets up this instance so that it is prepared for collecting metrics. Does
84 * so such that this setup method may be used on an instance that is already
85 * initialized, in a way that does not consume additional memory upon reuse.
86 * That can be useful if you decide to make your subclass of this mixin a
87 * "PooledClass".
88 */
89 reinitializeTransaction: function () {
90 this.transactionWrappers = this.getTransactionWrappers();
91 if (this.wrapperInitData) {
92 this.wrapperInitData.length = 0;
93 } else {
94 this.wrapperInitData = [];
95 }
96 this._isInTransaction = false;
97 },
98
99 _isInTransaction: false,
100
101 /**
102 * @abstract
103 * @return {Array<TransactionWrapper>} Array of transaction wrappers.
104 */
105 getTransactionWrappers: null,
106
107 isInTransaction: function () {
108 return !!this._isInTransaction;
109 },
110
111 /* eslint-disable space-before-function-paren */
112
113 /**
114 * Executes the function within a safety window. Use this for the top level
115 * methods that result in large amounts of computation/mutations that would
116 * need to be safety checked. The optional arguments helps prevent the need
117 * to bind in many cases.
118 *
119 * @param {function} method Member of scope to call.
120 * @param {Object} scope Scope to invoke from.
121 * @param {Object?=} a Argument to pass to the method.
122 * @param {Object?=} b Argument to pass to the method.
123 * @param {Object?=} c Argument to pass to the method.
124 * @param {Object?=} d Argument to pass to the method.
125 * @param {Object?=} e Argument to pass to the method.
126 * @param {Object?=} f Argument to pass to the method.
127 *
128 * @return {*} Return value from `method`.
129 */
130 perform: function (method, scope, a, b, c, d, e, f) {
131 /* eslint-enable space-before-function-paren */
132 !!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;
133 var errorThrown;
134 var ret;
135 try {
136 this._isInTransaction = true;
137 // Catching errors makes debugging more difficult, so we start with
138 // errorThrown set to true before setting it to false after calling
139 // close -- if it's still set to true in the finally block, it means
140 // one of these calls threw.
141 errorThrown = true;
142 this.initializeAll(0);
143 ret = method.call(scope, a, b, c, d, e, f);
144 errorThrown = false;
145 } finally {
146 try {
147 if (errorThrown) {
148 // If `method` throws, prefer to show that stack trace over any thrown
149 // by invoking `closeAll`.
150 try {
151 this.closeAll(0);
152 } catch (err) {}
153 } else {
154 // Since `method` didn't throw, we don't want to silence the exception
155 // here.
156 this.closeAll(0);
157 }
158 } finally {
159 this._isInTransaction = false;
160 }
161 }
162 return ret;
163 },
164
165 initializeAll: function (startIndex) {
166 var transactionWrappers = this.transactionWrappers;
167 for (var i = startIndex; i < transactionWrappers.length; i++) {
168 var wrapper = transactionWrappers[i];
169 try {
170 // Catching errors makes debugging more difficult, so we start with the
171 // OBSERVED_ERROR state before overwriting it with the real return value
172 // of initialize -- if it's still set to OBSERVED_ERROR in the finally
173 // block, it means wrapper.initialize threw.
174 this.wrapperInitData[i] = OBSERVED_ERROR;
175 this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
176 } finally {
177 if (this.wrapperInitData[i] === OBSERVED_ERROR) {
178 // The initializer for wrapper i threw an error; initialize the
179 // remaining wrappers but silence any exceptions from them to ensure
180 // that the first error is the one to bubble up.
181 try {
182 this.initializeAll(i + 1);
183 } catch (err) {}
184 }
185 }
186 }
187 },
188
189 /**
190 * Invokes each of `this.transactionWrappers.close[i]` functions, passing into
191 * them the respective return values of `this.transactionWrappers.init[i]`
192 * (`close`rs that correspond to initializers that failed will not be
193 * invoked).
194 */
195 closeAll: function (startIndex) {
196 !this.isInTransaction() ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Transaction.closeAll(): Cannot close transaction when none are open.') : _prodInvariant('28') : void 0;
197 var transactionWrappers = this.transactionWrappers;
198 for (var i = startIndex; i < transactionWrappers.length; i++) {
199 var wrapper = transactionWrappers[i];
200 var initData = this.wrapperInitData[i];
201 var errorThrown;
202 try {
203 // Catching errors makes debugging more difficult, so we start with
204 // errorThrown set to true before setting it to false after calling
205 // close -- if it's still set to true in the finally block, it means
206 // wrapper.close threw.
207 errorThrown = true;
208 if (initData !== OBSERVED_ERROR && wrapper.close) {
209 wrapper.close.call(this, initData);
210 }
211 errorThrown = false;
212 } finally {
213 if (errorThrown) {
214 // The closer for wrapper i threw an error; close the remaining
215 // wrappers but silence any exceptions from them to ensure that the
216 // first error is the one to bubble up.
217 try {
218 this.closeAll(i + 1);
219 } catch (e) {}
220 }
221 }
222 }
223 this.wrapperInitData.length = 0;
224 }
225};
226
227module.exports = TransactionImpl;
\No newline at end of file