@samitouri / QOS-React / commits / 2b226f5fa6

Revise ResponderTouchHistoryStore Error Handling (#7143)

Touch behavior is inconsistent across different platforms, and ResponderTouchHistoryStore currently fatals when assumptions are broken. In addition, the behavior differs between development and production. This pull request does a few things to make ResponderTouchHistoryStore easier to deal with: Adds Flow to keep the TouchEvent, Touch, and TouchRecord types straight. Changes behavior to be consistent across environments. This means either always throwing or never throwing (and making use of warning and console.error as appropriate). When an orphaned move or end event is received, print debug information and ignore it instead of crashing and burning.

Timothy Yung committed Jul 5, 2016 at 13:15 UTC 2b226f5fa673207ef49aeb05f4c578296e0640db
2 files changed +155 -112
src/renderers/shared/stack/event/eventPlugins/ResponderEventPlugin.js
+1 -1
@@ -496,7 +496,7 @@ var ResponderEventPlugin = {
496 );
497 }
498
499 - ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent, nativeEventTarget);
499 + ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
500
501 var extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent) ?
502 setResponderAndExtractTransfer(
src/renderers/shared/stack/event/eventPlugins/ResponderTouchHistoryStore.js
+154 -111
@@ -7,37 +7,44 @@
7 * of patent rights can be found in the PATENTS file in the same directory.
8 *
9 * @providesModule ResponderTouchHistoryStore
10 + * @flow
11 */
12
13 'use strict';
14
14 -var EventPluginUtils = require('EventPluginUtils');
15 +const EventPluginUtils = require('EventPluginUtils');
16
16 -var invariant = require('invariant');
17 +const invariant = require('invariant');
18 +const warning = require('warning');
19
18 -var isMoveish = EventPluginUtils.isMoveish;
19 -var isStartish = EventPluginUtils.isStartish;
20 -var isEndish = EventPluginUtils.isEndish;
21 -
22 -var MAX_TOUCH_BANK = 20;
20 +const {
21 + isEndish,
22 + isMoveish,
23 + isStartish,
24 +} = EventPluginUtils;
25
26 /**
25 - * Touch position/time tracking information by touchID. Typically, we'll only
26 - * see IDs with a range of 1-20 (they are recycled when touches end and then
27 - * start again). This data is commonly needed by many different interaction
28 - * logic modules so precomputing it is very helpful to do once.
29 - * Each touch object in `touchBank` is of the following form:
30 - * { touchActive: boolean,
31 - * startTimeStamp: number,
32 - * startPageX: number,
33 - * startPageY: number,
34 - * currentPageX: number,
35 - * currentPageY: number,
36 - * currentTimeStamp: number
37 - * }
27 + * Tracks the position and time of each active touch by `touch.identifier`. We
28 + * should typically only see IDs in the range of 1-20 because IDs get recycled
29 + * when touches end and start again.
30 */
39 -var touchHistory = {
40 - touchBank: [],
31 +type TouchRecord = {
32 + touchActive: boolean,
33 + startPageX: number,
34 + startPageY: number,
35 + startTimeStamp: number,
36 + currentPageX: number,
37 + currentPageY: number,
38 + currentTimeStamp: number,
39 + previousPageX: number,
40 + previousPageY: number,
41 + previousTimeStamp: number,
42 +};
43 +
44 +const MAX_TOUCH_BANK = 20;
45 +const touchBank: Array<TouchRecord> = [];
46 +const touchHistory = {
47 + touchBank,
48 numberActiveTouches: 0,
49 // If there is only one active touch, we remember its location. This prevents
50 // us having to loop through all of the touches all the time in the most
@@ -46,24 +53,34 @@ var touchHistory = {
53 mostRecentTimeStamp: 0,
54 };
55
49 -var timestampForTouch = function(touch) {
56 +type Touch = {
57 + identifier: ?number,
58 + pageX: number,
59 + pageY: number,
60 + timestamp: number,
61 +};
62 +type TouchEvent = {
63 + changedTouches: Array<Touch>,
64 + touches: Array<Touch>,
65 +};
66 +
67 +function timestampForTouch(touch: Touch): number {
68 // The legacy internal implementation provides "timeStamp", which has been
69 // renamed to "timestamp". Let both work for now while we iron it out
70 // TODO (evv): rename timeStamp to timestamp in internal code
53 - return touch.timeStamp || touch.timestamp;
54 -};
71 + return (touch: any).timeStamp || touch.timestamp;
72 +}
73
74 /**
75 * TODO: Instead of making gestures recompute filtered velocity, we could
76 * include a built in velocity computation that can be reused globally.
59 - * @param {Touch} touch Native touch object.
77 */
61 -var initializeTouchData = function(touch) {
78 +function createTouchRecord(touch: Touch): TouchRecord {
79 return {
80 touchActive: true,
64 - startTimeStamp: timestampForTouch(touch),
81 startPageX: touch.pageX,
82 startPageY: touch.pageY,
83 + startTimeStamp: timestampForTouch(touch),
84 currentPageX: touch.pageX,
85 currentPageY: touch.pageY,
86 currentTimeStamp: timestampForTouch(touch),
@@ -71,114 +88,140 @@ var initializeTouchData = function(touch) {
88 previousPageY: touch.pageY,
89 previousTimeStamp: timestampForTouch(touch),
90 };
74 -};
75 -
76 -var reinitializeTouchTrack = function(touchTrack, touch) {
77 - touchTrack.touchActive = true;
78 - touchTrack.startTimeStamp = timestampForTouch(touch);
79 - touchTrack.startPageX = touch.pageX;
80 - touchTrack.startPageY = touch.pageY;
81 - touchTrack.currentPageX = touch.pageX;
82 - touchTrack.currentPageY = touch.pageY;
83 - touchTrack.currentTimeStamp = timestampForTouch(touch);
84 - touchTrack.previousPageX = touch.pageX;
85 - touchTrack.previousPageY = touch.pageY;
86 - touchTrack.previousTimeStamp = timestampForTouch(touch);
87 -};
88 -
89 -var validateTouch = function(touch) {
90 - var identifier = touch.identifier;
91 - invariant(identifier != null, 'Touch object is missing identifier');
92 - if (identifier > MAX_TOUCH_BANK) {
93 - console.warn(
94 - 'Touch identifier ' + identifier + ' is greater than maximum ' +
95 - 'supported ' + MAX_TOUCH_BANK + ' which causes performance issues ' +
96 - 'backfilling array locations for all of the indices.'
97 - );
98 - }
99 -};
100 -
101 -var recordStartTouchData = function(touch) {
102 - var touchBank = touchHistory.touchBank;
103 - var identifier = touch.identifier;
104 - var touchTrack = touchBank[identifier];
105 - if (__DEV__) {
106 - validateTouch(touch);
107 - }
108 - if (touchTrack) {
109 - reinitializeTouchTrack(touchTrack, touch);
91 +}
92 +
93 +function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
94 + touchRecord.touchActive = true;
95 + touchRecord.startPageX = touch.pageX;
96 + touchRecord.startPageY = touch.pageY;
97 + touchRecord.startTimeStamp = timestampForTouch(touch);
98 + touchRecord.currentPageX = touch.pageX;
99 + touchRecord.currentPageY = touch.pageY;
100 + touchRecord.currentTimeStamp = timestampForTouch(touch);
101 + touchRecord.previousPageX = touch.pageX;
102 + touchRecord.previousPageY = touch.pageY;
103 + touchRecord.previousTimeStamp = timestampForTouch(touch);
104 +}
105 +
106 +function getTouchIdentifier({identifier}: Touch): number {
107 + invariant(identifier != null, 'Touch object is missing identifier.');
108 + warning(
109 + identifier <= MAX_TOUCH_BANK,
110 + 'Touch identifier %s is greater than maximum supported %s which causes ' +
111 + 'performance issues backfilling array locations for all of the indices.',
112 + identifier,
113 + MAX_TOUCH_BANK
114 + );
115 + return identifier;
116 +}
117 +
118 +function recordTouchStart(touch: Touch): void {
119 + const identifier = getTouchIdentifier(touch);
120 + const touchRecord = touchBank[identifier];
121 + if (touchRecord) {
122 + resetTouchRecord(touchRecord, touch);
123 } else {
111 - touchBank[touch.identifier] = initializeTouchData(touch);
124 + touchBank[identifier] = createTouchRecord(touch);
125 }
126 touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
114 -};
115 -
116 -var recordMoveTouchData = function(touch) {
117 - var touchBank = touchHistory.touchBank;
118 - var touchTrack = touchBank[touch.identifier];
119 - if (__DEV__) {
120 - validateTouch(touch);
121 - invariant(touchTrack, 'Touch data should have been recorded on start');
127 +}
128 +
129 +function recordTouchMove(touch: Touch): void {
130 + const touchRecord = touchBank[getTouchIdentifier(touch)];
131 + if (touchRecord) {
132 + touchRecord.touchActive = true;
133 + touchRecord.previousPageX = touchRecord.currentPageX;
134 + touchRecord.previousPageY = touchRecord.currentPageY;
135 + touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
136 + touchRecord.currentPageX = touch.pageX;
137 + touchRecord.currentPageY = touch.pageY;
138 + touchRecord.currentTimeStamp = timestampForTouch(touch);
139 + touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
140 + } else {
141 + console.error(
142 + 'Cannot record touch move without a touch start.\n' +
143 + 'Touch Move: %s\n',
144 + 'Touch Bank: %s',
145 + printTouch(touch),
146 + printTouchBank()
147 + );
148 }
123 - touchTrack.touchActive = true;
124 - touchTrack.previousPageX = touchTrack.currentPageX;
125 - touchTrack.previousPageY = touchTrack.currentPageY;
126 - touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
127 - touchTrack.currentPageX = touch.pageX;
128 - touchTrack.currentPageY = touch.pageY;
129 - touchTrack.currentTimeStamp = timestampForTouch(touch);
130 - touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
131 -};
132 -
133 -var recordEndTouchData = function(touch) {
134 - var touchBank = touchHistory.touchBank;
135 - var touchTrack = touchBank[touch.identifier];
136 - if (__DEV__) {
137 - validateTouch(touch);
138 - invariant(touchTrack, 'Touch data should have been recorded on start');
149 +}
150 +
151 +function recordTouchEnd(touch: Touch): void {
152 + const touchRecord = touchBank[getTouchIdentifier(touch)];
153 + if (touchRecord) {
154 + touchRecord.touchActive = false;
155 + touchRecord.previousPageX = touchRecord.currentPageX;
156 + touchRecord.previousPageY = touchRecord.currentPageY;
157 + touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
158 + touchRecord.currentPageX = touch.pageX;
159 + touchRecord.currentPageY = touch.pageY;
160 + touchRecord.currentTimeStamp = timestampForTouch(touch);
161 + touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
162 + } else {
163 + console.error(
164 + 'Cannot record touch end without a touch start.\n' +
165 + 'Touch End: %s\n',
166 + 'Touch Bank: %s',
167 + printTouch(touch),
168 + printTouchBank()
169 + );
170 }
140 - touchTrack.previousPageX = touchTrack.currentPageX;
141 - touchTrack.previousPageY = touchTrack.currentPageY;
142 - touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
143 - touchTrack.currentPageX = touch.pageX;
144 - touchTrack.currentPageY = touch.pageY;
145 - touchTrack.currentTimeStamp = timestampForTouch(touch);
146 - touchTrack.touchActive = false;
147 - touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
148 -};
171 +}
172 +
173 +function printTouch(touch: Touch): string {
174 + return JSON.stringify({
175 + identifier: touch.identifier,
176 + pageX: touch.pageX,
177 + pageY: touch.pageY,
178 + timestamp: timestampForTouch(touch),
179 + });
180 +}
181 +
182 +function printTouchBank(): string {
183 + let printed = JSON.stringify(touchBank.slice(0, MAX_TOUCH_BANK));
184 + if (touchBank.length > MAX_TOUCH_BANK) {
185 + printed += ' (original size: ' + touchBank.length + ')';
186 + }
187 + return printed;
188 +}
189
150 -var ResponderTouchHistoryStore = {
151 - recordTouchTrack: function(topLevelType, nativeEvent) {
152 - var touchBank = touchHistory.touchBank;
190 +const ResponderTouchHistoryStore = {
191 + recordTouchTrack(topLevelType: string, nativeEvent: TouchEvent): void {
192 if (isMoveish(topLevelType)) {
154 - nativeEvent.changedTouches.forEach(recordMoveTouchData);
193 + nativeEvent.changedTouches.forEach(recordTouchMove);
194 } else if (isStartish(topLevelType)) {
156 - nativeEvent.changedTouches.forEach(recordStartTouchData);
195 + nativeEvent.changedTouches.forEach(recordTouchStart);
196 touchHistory.numberActiveTouches = nativeEvent.touches.length;
197 if (touchHistory.numberActiveTouches === 1) {
159 - touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier;
198 + touchHistory.indexOfSingleActiveTouch =
199 + nativeEvent.touches[0].identifier;
200 }
201 } else if (isEndish(topLevelType)) {
162 - nativeEvent.changedTouches.forEach(recordEndTouchData);
202 + nativeEvent.changedTouches.forEach(recordTouchEnd);
203 touchHistory.numberActiveTouches = nativeEvent.touches.length;
204 if (touchHistory.numberActiveTouches === 1) {
165 - for (var i = 0; i < touchBank.length; i++) {
166 - var touchTrackToCheck = touchBank[i];
205 + for (let i = 0; i < touchBank.length; i++) {
206 + const touchTrackToCheck = touchBank[i];
207 if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
208 touchHistory.indexOfSingleActiveTouch = i;
209 break;
210 }
211 }
212 if (__DEV__) {
173 - var activeTouchData = touchBank[touchHistory.indexOfSingleActiveTouch];
174 - var foundActive = activeTouchData != null && !!activeTouchData.touchActive;
175 - invariant(foundActive, 'Cannot find single active touch');
213 + const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
214 + warning(
215 + activeRecord != null &&
216 + activeRecord.touchActive,
217 + 'Cannot find single active touch.'
218 + );
219 }
220 }
221 }
222 },
223
181 - touchHistory: touchHistory,
224 + touchHistory,
225 };
226
227