main
js 263 lines 8.72 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
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 * @flow
8 */
9
10 import type {
11 Thenable,
12 FulfilledThenable,
13 RejectedThenable,
14 } from 'shared/ReactTypes';
15 import type {Lane} from './ReactFiberLane';
16 import type {Transition} from 'react/src/ReactStartTransition';
17
18 import {
19 requestTransitionLane,
20 ensureScheduleIsScheduled,
21 } from './ReactFiberRootScheduler';
22 import {NoLane} from './ReactFiberLane';
23 import {
24 hasScheduledTransitionWork,
25 clearAsyncTransitionTimer,
26 } from './ReactProfilerTimer';
27 import {
28 enableComponentPerformanceTrack,
29 enableProfilerTimer,
30 enableDefaultTransitionIndicator,
31 } from 'shared/ReactFeatureFlags';
32 import {clearEntangledAsyncTransitionTypes} from './ReactFiberTransitionTypes';
33
34 import noop from 'shared/noop';
35 import reportGlobalError from 'shared/reportGlobalError';
36
37 // If there are multiple, concurrent async actions, they are entangled. All
38 // transition updates that occur while the async action is still in progress
39 // are treated as part of the action.
40 //
41 // The ideal behavior would be to treat each async function as an independent
42 // action. However, without a mechanism like AsyncContext, we can't tell which
43 // action an update corresponds to. So instead, we entangle them all into one.
44
45 // The listeners to notify once the entangled scope completes.
46 let currentEntangledListeners: Array<() => mixed> | null = null;
47 // The number of pending async actions in the entangled scope.
48 let currentEntangledPendingCount: number = 0;
49 // The transition lane shared by all updates in the entangled scope.
50 let currentEntangledLane: Lane = NoLane;
51 // A thenable that resolves when the entangled scope completes. It does not
52 // resolve to a particular value because it's only used for suspending the UI
53 // until the async action scope has completed.
54 let currentEntangledActionThenable: Thenable<void> | null = null;
55
56 // Track the default indicator for every root. undefined means we haven't
57 // had any roots registered yet. null means there's more than one callback.
58 // If there's more than one callback we bailout to not supporting isomorphic
59 // default indicators.
60 let isomorphicDefaultTransitionIndicator:
61 | void
62 | null
63 | (() => void | (() => void)) = undefined;
64 // The clean up function for the currently running indicator.
65 let pendingIsomorphicIndicator: null | (() => void) = null;
66 // The number of roots that have pending Transitions that depend on the
67 // started isomorphic indicator.
68 let pendingEntangledRoots: number = 0;
69 let needsIsomorphicIndicator: boolean = false;
70
71 export function entangleAsyncAction<S>(
72 transition: Transition,
73 thenable: Thenable<S>,
74 ): Thenable<S> {
75 // `thenable` is the return value of the async action scope function. Create
76 // a combined thenable that resolves once every entangled scope function
77 // has finished.
78 if (currentEntangledListeners === null) {
79 // There's no outer async action scope. Create a new one.
80 const entangledListeners = (currentEntangledListeners = []);
81 currentEntangledPendingCount = 0;
82 currentEntangledLane = requestTransitionLane(transition);
83 const entangledThenable: Thenable<void> = {
84 status: 'pending',
85 value: undefined,
86 then(resolve: void => mixed) {
87 entangledListeners.push(resolve);
88 },
89 };
90 currentEntangledActionThenable = entangledThenable;
91 if (enableDefaultTransitionIndicator) {
92 needsIsomorphicIndicator = true;
93 // We'll check if we need a default indicator in a microtask. Ensure
94 // we have this scheduled even if no root is scheduled.
95 ensureScheduleIsScheduled();
96 }
97 }
98 currentEntangledPendingCount++;
99 thenable.then(pingEngtangledActionScope, pingEngtangledActionScope);
100 return thenable;
101 }
102
103 function pingEngtangledActionScope() {
104 if (--currentEntangledPendingCount === 0) {
105 if (enableProfilerTimer && enableComponentPerformanceTrack) {
106 if (!hasScheduledTransitionWork()) {
107 // If we have received no updates since we started the entangled Actions
108 // that means it didn't lead to a Transition being rendered. We need to
109 // clear the timer so that if we start another entangled sequence we use
110 // the next start timer instead of appearing like we were blocked the
111 // whole time. We currently don't log a track for Actions that don't
112 // render a Transition.
113 clearAsyncTransitionTimer();
114 }
115 }
116 clearEntangledAsyncTransitionTypes();
117 if (pendingEntangledRoots === 0) {
118 stopIsomorphicDefaultIndicator();
119 }
120 if (currentEntangledListeners !== null) {
121 // All the actions have finished. Close the entangled async action scope
122 // and notify all the listeners.
123 if (currentEntangledActionThenable !== null) {
124 const fulfilledThenable: FulfilledThenable<void> =
125 currentEntangledActionThenable as any;
126 fulfilledThenable.status = 'fulfilled';
127 }
128 const listeners = currentEntangledListeners;
129 currentEntangledListeners = null;
130 currentEntangledLane = NoLane;
131 currentEntangledActionThenable = null;
132 needsIsomorphicIndicator = false;
133 for (let i = 0; i < listeners.length; i++) {
134 const listener = listeners[i];
135 listener();
136 }
137 }
138 }
139 }
140
141 export function chainThenableValue<T>(
142 thenable: Thenable<T>,
143 result: T,
144 ): Thenable<T> {
145 // Equivalent to: Promise.resolve(thenable).then(() => result), except we can
146 // cheat a bit since we know that that this thenable is only ever consumed
147 // by React.
148 //
149 // We don't technically require promise support on the client yet, hence this
150 // extra code.
151 const listeners = [];
152 const thenableWithOverride: Thenable<T> = {
153 status: 'pending',
154 value: null,
155 reason: null,
156 then(resolve: T => mixed) {
157 listeners.push(resolve);
158 },
159 };
160 thenable.then(
161 (value: T) => {
162 const fulfilledThenable: FulfilledThenable<T> =
163 thenableWithOverride as any;
164 fulfilledThenable.status = 'fulfilled';
165 fulfilledThenable.value = result;
166 for (let i = 0; i < listeners.length; i++) {
167 const listener = listeners[i];
168 listener(result);
169 }
170 },
171 error => {
172 const rejectedThenable: RejectedThenable<T> = thenableWithOverride as any;
173 rejectedThenable.status = 'rejected';
174 rejectedThenable.reason = error;
175 for (let i = 0; i < listeners.length; i++) {
176 const listener = listeners[i];
177 // This is a perf hack where we call the `onFulfill` ping function
178 // instead of `onReject`, because we know that React is the only
179 // consumer of these promises, and it passes the same listener to both.
180 // We also know that it will read the error directly off the
181 // `.reason` field.
182 listener(undefined as any);
183 }
184 },
185 );
186 return thenableWithOverride;
187 }
188
189 export function peekEntangledActionLane(): Lane {
190 return currentEntangledLane;
191 }
192
193 export function peekEntangledActionThenable(): Thenable<void> | null {
194 return currentEntangledActionThenable;
195 }
196
197 export function registerDefaultIndicator(
198 onDefaultTransitionIndicator: () => void | (() => void),
199 ): void {
200 if (!enableDefaultTransitionIndicator) {
201 return;
202 }
203 if (isomorphicDefaultTransitionIndicator === undefined) {
204 isomorphicDefaultTransitionIndicator = onDefaultTransitionIndicator;
205 } else if (
206 isomorphicDefaultTransitionIndicator !== onDefaultTransitionIndicator
207 ) {
208 isomorphicDefaultTransitionIndicator = null;
209 // Stop any on-going indicator since it's now ambiguous.
210 stopIsomorphicDefaultIndicator();
211 }
212 }
213
214 export function startIsomorphicDefaultIndicatorIfNeeded() {
215 if (!enableDefaultTransitionIndicator) {
216 return;
217 }
218 if (!needsIsomorphicIndicator) {
219 return;
220 }
221 if (
222 isomorphicDefaultTransitionIndicator != null &&
223 pendingIsomorphicIndicator === null
224 ) {
225 try {
226 pendingIsomorphicIndicator =
227 isomorphicDefaultTransitionIndicator() || noop;
228 } catch (x) {
229 pendingIsomorphicIndicator = noop;
230 reportGlobalError(x);
231 }
232 }
233 }
234
235 function stopIsomorphicDefaultIndicator() {
236 if (!enableDefaultTransitionIndicator) {
237 return;
238 }
239 if (pendingIsomorphicIndicator !== null) {
240 const cleanup = pendingIsomorphicIndicator;
241 pendingIsomorphicIndicator = null;
242 cleanup();
243 }
244 }
245
246 function releaseIsomorphicIndicator() {
247 if (--pendingEntangledRoots === 0) {
248 stopIsomorphicDefaultIndicator();
249 }
250 }
251
252 export function hasOngoingIsomorphicIndicator(): boolean {
253 return pendingIsomorphicIndicator !== null;
254 }
255
256 export function retainIsomorphicIndicator(): () => void {
257 pendingEntangledRoots++;
258 return releaseIsomorphicIndicator;
259 }
260
261 export function markIsomorphicIndicatorHandled(): void {
262 needsIsomorphicIndicator = false;
263 }