| 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 | } |