@@ -11,6 +11,8 @@ import {type RenderStreamContextValue} from './context.js'
1111import { RenderStreamContextProvider } from './context.js'
1212import { syncQueries , type Queries , type SyncQueries } from './syncQueries.js'
1313
14+ let nextProfilerId = 0
15+
1416export type ValidSnapshot =
1517 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
1618 void | ( object & { /* not a function */ call ?: never } )
@@ -146,6 +148,8 @@ export function createRenderStream<
146148 resolveNextRender = undefined
147149 rejectNextRender = undefined
148150 }
151+ let effectsFlushedCount = 0
152+ const pendingCommits = new Map < string , number > ( )
149153 const snapshotRef = { current : initialSnapshot }
150154 const replaceSnapshot : ReplaceSnapshot < Snapshot > = snap => {
151155 if ( typeof snap === 'function' ) {
@@ -230,24 +234,47 @@ export function createRenderStream<
230234 )
231235 renderStreamContext . renderedComponents = [ ]
232236 stream . renders . push ( render )
233- resolveNextRender ?.( render )
234237 } catch ( error ) {
235238 stream . renders . push ( {
236239 phase : 'snapshotError' ,
237240 count : stream . renders . length ,
238241 error,
239242 } )
240- rejectNextRender ?.( error )
241243 } finally {
242- resetNextRender ( )
244+ pendingCommits . set ( `${ id } :${ commitTime } ` , stream . renders . length )
245+ }
246+ }
247+
248+ function profilerOnPostCommit (
249+ id : string ,
250+ _phase : string ,
251+ _passiveEffectDuration : number ,
252+ commitTime : number ,
253+ ) {
254+ const key = `${ id } :${ commitTime } `
255+ const renderCount = pendingCommits . get ( key )
256+ if ( renderCount === undefined ) return
257+ pendingCommits . delete ( key )
258+ const render = stream . renders [ effectsFlushedCount ]
259+ effectsFlushedCount = renderCount
260+ if ( render . phase === 'snapshotError' ) {
261+ rejectNextRender ?.( render . error )
262+ } else {
263+ resolveNextRender ?.( render )
243264 }
265+ resetNextRender ( )
244266 }
245267
246268 let iteratorPosition = 0
247- function Wrapper ( { children} : { children : React . ReactNode } ) {
269+ function Wrapper ( { children, id } : { children : React . ReactNode ; id : string } ) {
248270 return (
249271 < RenderStreamContextProvider value = { renderStreamContext } >
250- < React . Profiler id = "test" onRender = { profilerOnRender } >
272+ < React . Profiler
273+ id = { id }
274+ onRender = { profilerOnRender }
275+ // Supported in React 18+ development/profiling builds, but untyped.
276+ { ...{ onPostCommit : profilerOnPostCommit } }
277+ >
251278 { children }
252279 </ React . Profiler >
253280 </ RenderStreamContextProvider >
@@ -258,31 +285,32 @@ export function createRenderStream<
258285 ui : React . ReactNode ,
259286 options ?: RenderOptions < any , any , any > ,
260287 ) => {
288+ const id = `render-stream-${ nextProfilerId ++ } `
289+ const previousRenderCount = stream . renders . length
261290 const ret = await renderWithoutAct ( ui , {
262291 ...options ,
263292 wrapper : props => {
264293 const ParentWrapper = options ?. wrapper ?? React . Fragment
265294 return (
266295 < ParentWrapper >
267- < Wrapper > { props . children } </ Wrapper >
296+ < Wrapper id = { id } > { props . children } </ Wrapper >
268297 </ ParentWrapper >
269298 )
270299 } ,
271300 } )
272- if ( stream . renders . length === 0 ) {
301+ // eslint-disable-next-line no-unmodified-loop-condition -- updated by onPostCommit
302+ while ( effectsFlushedCount <= previousRenderCount ) {
303+ // eslint-disable-next-line no-await-in-loop
273304 await stream . waitForNextRender ( )
274305 }
275306 const origRerender = ret . rerender
276307 ret . rerender = async function rerender ( rerenderUi : React . ReactNode ) {
277- const previousRenderCount = stream . renders . length
278- try {
279- return await origRerender ( rerenderUi )
280- } finally {
281- // only wait for the next render if the rerender was not
282- // synchronous (React 17)
283- if ( previousRenderCount === stream . renders . length ) {
284- await stream . waitForNextRender ( )
285- }
308+ const rerenderPosition = stream . renders . length
309+ await origRerender ( rerenderUi )
310+ // eslint-disable-next-line no-unmodified-loop-condition -- updated by onPostCommit
311+ while ( effectsFlushedCount <= rerenderPosition ) {
312+ // eslint-disable-next-line no-await-in-loop
313+ await stream . waitForNextRender ( )
286314 }
287315 }
288316 return ret
@@ -299,27 +327,18 @@ export function createRenderStream<
299327 return stream . renders . length
300328 } ,
301329 async peekRender ( options : NextRenderOptions = { } ) {
302- try {
303- if ( iteratorPosition < stream . renders . length ) {
304- const peekedRender = stream . renders [ iteratorPosition ]
305-
306- if ( peekedRender . phase === 'snapshotError' ) {
307- throw peekedRender . error
308- }
330+ if ( iteratorPosition < effectsFlushedCount ) {
331+ const peekedRender = stream . renders [ iteratorPosition ]
309332
310- return peekedRender
333+ if ( peekedRender . phase === 'snapshotError' ) {
334+ throw peekedRender . error
311335 }
312- return await stream
313- . waitForNextRender ( options )
314- . catch ( rethrowWithCapturedStackTrace ( stream . peekRender ) )
315- } finally {
316- /** drain microtask queue */
317- await new Promise < void > ( resolve => {
318- setTimeout ( ( ) => {
319- resolve ( )
320- } , 0 )
321- } )
336+
337+ return peekedRender
322338 }
339+ return stream
340+ . waitForNextRender ( options )
341+ . catch ( rethrowWithCapturedStackTrace ( stream . peekRender ) )
323342 } ,
324343 takeRender : markAssertable ( async function takeRender (
325344 options : NextRenderOptions = { } ,
0 commit comments