@@ -52,30 +52,37 @@ export class StagedRenderingController {
5252 }
5353 }
5454
55- delayUntilStage < T > ( stage : NonStaticRenderStage , resolvedValue : T ) {
56- let stagePromise : Promise < void >
55+ private getStagePromise ( stage : NonStaticRenderStage ) : Promise < void > {
5756 switch ( stage ) {
5857 case RenderStage . Runtime : {
59- stagePromise = this . runtimeStagePromise . promise
60- break
58+ return this . runtimeStagePromise . promise
6159 }
6260 case RenderStage . Dynamic : {
63- stagePromise = this . dynamicStagePromise . promise
64- break
61+ return this . dynamicStagePromise . promise
6562 }
6663 default : {
6764 stage satisfies never
6865 throw new InvariantError ( `Invalid render stage: ${ stage } ` )
6966 }
7067 }
68+ }
69+
70+ waitForStage ( stage : NonStaticRenderStage ) {
71+ return this . getStagePromise ( stage )
72+ }
73+
74+ delayUntilStage < T > (
75+ stage : NonStaticRenderStage ,
76+ displayName : string | undefined ,
77+ resolvedValue : T
78+ ) {
79+ const ioTriggerPromise = this . getStagePromise ( stage )
7180
72- // FIXME: this seems to be the only form that leads to correct API names
73- // being displayed in React Devtools (in the "suspended by" section).
74- // If we use `promise.then(() => resolvedValue)`, the names are lost.
75- // It's a bit strange that only one of those works right.
76- const promise = new Promise < T > ( ( resolve , reject ) => {
77- stagePromise . then ( resolve . bind ( null , resolvedValue ) , reject )
78- } )
81+ const promise = makeDevtoolsIOPromiseFromIOTrigger (
82+ ioTriggerPromise ,
83+ displayName ,
84+ resolvedValue
85+ )
7986
8087 // Analogously to `makeHangingPromise`, we might reject this promise if the signal is invoked.
8188 // (e.g. in the case where we don't want want the render to proceed to the dynamic stage and abort it).
@@ -88,3 +95,26 @@ export class StagedRenderingController {
8895}
8996
9097function ignoreReject ( ) { }
98+
99+ // TODO(restart-on-cache-miss): the layering of `delayUntilStage`,
100+ // `makeDevtoolsIOPromiseFromIOTrigger` and and `makeDevtoolsIOAwarePromise`
101+ // is confusing, we should clean it up.
102+ function makeDevtoolsIOPromiseFromIOTrigger < T > (
103+ ioTrigger : Promise < any > ,
104+ displayName : string | undefined ,
105+ resolvedValue : T
106+ ) : Promise < T > {
107+ // If we create a `new Promise` and give it a displayName
108+ // (with no userspace code above us in the stack)
109+ // React Devtools will use it as the IO cause when determining "suspended by".
110+ // In particular, it should shadow any inner IO that resolved/rejected the promise
111+ // (in case of staged rendering, this will be the `setTimeout` that triggers the relevant stage)
112+ const promise = new Promise < T > ( ( resolve , reject ) => {
113+ ioTrigger . then ( resolve . bind ( null , resolvedValue ) , reject )
114+ } )
115+ if ( displayName !== undefined ) {
116+ // @ts -expect-error
117+ promise . displayName = displayName
118+ }
119+ return promise
120+ }
0 commit comments