UNPKG

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