@@ -11,8 +11,6 @@ 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-
1614export type ValidSnapshot =
1715 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
1816 void | ( object & { /* not a function */ call ?: never } )
@@ -140,19 +138,20 @@ export function createRenderStream<
140138 // created, which is important for the `markAssertable` function
141139 const stream = { } as any as RenderStreamWithRenderFn < Snapshot , Q >
142140
141+ type Commit = PromiseWithResolvers <
142+ ( typeof stream . renders ) [ number ] | undefined
143+ > & {
144+ finalize ?: ( ) => ( typeof stream . renders ) [ number ]
145+ }
143146 let nextRender : Promise < Render < Snapshot , Q > > | undefined ,
144- resolveNextRender : ( ( render : Render < Snapshot , Q > ) => void ) | undefined ,
145- rejectNextRender : ( ( error : unknown ) => void ) | undefined ,
146- nextRenderTimeout : ReturnType < typeof setTimeout > | undefined
147- function resetNextRender ( ) {
148- clearTimeout ( nextRenderTimeout )
149- nextRenderTimeout = undefined
150- nextRender = undefined
151- resolveNextRender = undefined
152- rejectNextRender = undefined
147+ upcomingCommit : Commit | undefined
148+ const pendingCommits : Array < { commitTime : number ; commit : Commit } > = [ ]
149+
150+ function nextCommit ( ) : Commit {
151+ return ( upcomingCommit ??=
152+ Promise . withResolvers < Awaited < Commit [ 'promise' ] > > ( ) )
153153 }
154- let effectsFlushedCount = 0
155- const pendingCommits = new Map < string , number > ( )
154+
156155 const snapshotRef = { current : initialSnapshot }
157156 const replaceSnapshot : ReplaceSnapshot < Snapshot > = snap => {
158157 if ( typeof snap === 'function' ) {
@@ -193,6 +192,16 @@ export function createRenderStream<
193192 startTime ,
194193 commitTime ,
195194 ) => {
195+ const commit = nextCommit ( )
196+ upcomingCommit = undefined
197+ pendingCommits . push ( { commitTime, commit} )
198+ if ( / ^ ( 1 6 | 1 7 ) \. / . test ( React . version ) ) {
199+ // React 16/17 have no onPostCommit hook. Let the commit finish scheduling
200+ // passive effects before yielding a task for them to run.
201+ void Promise . resolve ( ) . then ( ( ) => {
202+ setTimeout ( ( ) => profilerOnPostCommit ( id , phase , 0 , commitTime ) , 0 )
203+ } )
204+ }
196205 if (
197206 skipNonTrackingRenders &&
198207 renderStreamContext . renderedComponents . length === 0
@@ -207,7 +216,10 @@ export function createRenderStream<
207216 baseDuration,
208217 startTime,
209218 commitTime,
210- count : stream . renders . length + 1 ,
219+ count :
220+ stream . renders . length +
221+ pendingCommits . filter ( pending => pending . commit . finalize ) . length +
222+ 1 ,
211223 }
212224 try {
213225 /*
@@ -236,51 +248,43 @@ export function createRenderStream<
236248 queries ,
237249 )
238250 renderStreamContext . renderedComponents = [ ]
239- stream . renders . push ( render )
251+ commit . finalize = ( ) => {
252+ stream . renders . push ( render )
253+ return render
254+ }
240255 } catch ( error ) {
241- stream . renders . push ( {
242- phase : 'snapshotError' ,
243- count : stream . renders . length ,
244- error,
245- } )
246- } finally {
247- pendingCommits . set ( `${ id } :${ commitTime } ` , stream . renders . length )
248- if ( / ^ ( 1 6 | 1 7 ) \. / . test ( React . version ) ) {
249- // React 16/17 have no onPostCommit hook. Let the commit finish scheduling
250- // passive effects before yielding a task for them to run.
251- void Promise . resolve ( ) . then ( ( ) => {
252- setTimeout ( ( ) => profilerOnPostCommit ( id , phase , 0 , commitTime ) , 0 )
253- } )
256+ commit . finalize = ( ) => {
257+ const err = {
258+ phase : 'snapshotError' as const ,
259+ count : stream . renders . length ,
260+ error,
261+ }
262+ stream . renders . push ( err )
263+ return err
254264 }
255265 }
256266 }
257267
258268 function profilerOnPostCommit (
259- id : string ,
269+ _id : string ,
260270 _phase : string ,
261271 _passiveEffectDuration : number ,
262272 commitTime : number ,
263273 ) {
264- const key = `${ id } :${ commitTime } `
265- const renderCount = pendingCommits . get ( key )
266- if ( renderCount === undefined ) return
267- pendingCommits . delete ( key )
268- const render = stream . renders [ effectsFlushedCount ]
269- effectsFlushedCount = renderCount
270- if ( render . phase === 'snapshotError' ) {
271- rejectNextRender ?.( render . error )
272- } else {
273- resolveNextRender ?.( render )
274- }
275- resetNextRender ( )
274+ const index = pendingCommits . findIndex (
275+ pending => pending . commitTime === commitTime ,
276+ )
277+ if ( index === - 1 ) return
278+ const { commit} = pendingCommits . splice ( index , 1 ) [ 0 ]
279+ commit . resolve ( commit . finalize ?.( ) )
276280 }
277281
278282 let iteratorPosition = 0
279- function Wrapper ( { children, id } : { children : React . ReactNode ; id : string } ) {
283+ function Wrapper ( { children} : { children : React . ReactNode } ) {
280284 return (
281285 < RenderStreamContextProvider value = { renderStreamContext } >
282286 < React . Profiler
283- id = { id }
287+ id = "test"
284288 onRender = { profilerOnRender }
285289 // Supported in React 18+ development/profiling builds, but untyped.
286290 { ...{ onPostCommit : profilerOnPostCommit } }
@@ -295,33 +299,24 @@ export function createRenderStream<
295299 ui : React . ReactNode ,
296300 options ?: RenderOptions < any , any , any > ,
297301 ) => {
298- const id = `render-stream-${ nextProfilerId ++ } `
299- const previousRenderCount = stream . renders . length
302+ const commit = nextCommit ( )
300303 const ret = await renderWithoutAct ( ui , {
301304 ...options ,
302305 wrapper : props => {
303306 const ParentWrapper = options ?. wrapper ?? React . Fragment
304307 return (
305308 < ParentWrapper >
306- < Wrapper id = { id } > { props . children } </ Wrapper >
309+ < Wrapper > { props . children } </ Wrapper >
307310 </ ParentWrapper >
308311 )
309312 } ,
310313 } )
311- // eslint-disable-next-line no-unmodified-loop-condition -- updated by onPostCommit
312- while ( effectsFlushedCount <= previousRenderCount ) {
313- // eslint-disable-next-line no-await-in-loop
314- await stream . waitForNextRender ( )
315- }
314+ await waitForCommit ( commit )
316315 const origRerender = ret . rerender
317316 ret . rerender = async function rerender ( rerenderUi : React . ReactNode ) {
318- const rerenderPosition = stream . renders . length
317+ const rerenderCommit = nextCommit ( )
319318 await origRerender ( rerenderUi )
320- // eslint-disable-next-line no-unmodified-loop-condition -- updated by onPostCommit
321- while ( effectsFlushedCount <= rerenderPosition ) {
322- // eslint-disable-next-line no-await-in-loop
323- await stream . waitForNextRender ( )
324- }
319+ await waitForCommit ( rerenderCommit )
325320 }
326321 return ret
327322 } ) as unknown as RenderWithoutActAsync // TODO
@@ -337,7 +332,7 @@ export function createRenderStream<
337332 return stream . renders . length
338333 } ,
339334 async peekRender ( options : NextRenderOptions = { } ) {
340- if ( iteratorPosition < effectsFlushedCount ) {
335+ if ( iteratorPosition < stream . renders . length ) {
341336 const peekedRender = stream . renders [ iteratorPosition ]
342337
343338 if ( peekedRender . phase === 'snapshotError' ) {
@@ -394,26 +389,59 @@ export function createRenderStream<
394389 } ,
395390 waitForNextRender ( { timeout = 1000 } : NextRenderOptions = { } ) {
396391 if ( ! nextRender ) {
392+ const position = stream . renders . length
393+ const wait = { active : true }
394+ let timer : ReturnType < typeof setTimeout > | undefined
397395 nextRender = Promise . race < Render < Snapshot , Q > > ( [
398- new Promise < Render < Snapshot , Q > > ( ( resolve , reject ) => {
399- resolveNextRender = resolve
400- rejectNextRender = reject
401- } ) ,
396+ ( async ( ) => {
397+ while ( wait . active && stream . renders . length === position ) {
398+ // A skipped commit completes without publishing a snapshot.
399+ // eslint-disable-next-line no-await-in-loop
400+ await ( pendingCommits [ 0 ] ?. commit ?? nextCommit ( ) ) . promise
401+ }
402+ if ( ! wait . active ) throw new WaitForRenderTimeoutError ( )
403+ const next = stream . renders [ position ]
404+ if ( next . phase === 'snapshotError' ) throw next . error
405+ return next
406+ } ) ( ) ,
402407 new Promise < Render < Snapshot , Q > > ( ( _ , reject ) => {
403- nextRenderTimeout = setTimeout ( ( ) => {
408+ timer = setTimeout ( ( ) => {
404409 const error = new WaitForRenderTimeoutError ( )
405410 Error . captureStackTrace ( error , stream . waitForNextRender )
406411 reject ( error )
407- resetNextRender ( )
408412 } , timeout )
409413 } ) ,
410- ] )
414+ ] ) . finally ( ( ) => {
415+ wait . active = false
416+ clearTimeout ( timer )
417+ nextRender = undefined
418+ } )
411419 }
412420 return nextRender
413421 } ,
414422 render,
415423 } )
416424 return stream
425+
426+ async function waitForCommit ( commit : Commit ) {
427+ let timer : ReturnType < typeof setTimeout > | undefined
428+ try {
429+ return await Promise . race ( [
430+ commit . promise . then ( result => {
431+ if ( result ?. phase === 'snapshotError' ) throw result . error
432+ return result
433+ } ) ,
434+ new Promise < never > ( ( _ , reject ) => {
435+ timer = setTimeout (
436+ ( ) => reject ( new WaitForRenderTimeoutError ( ) ) ,
437+ 1000 ,
438+ )
439+ } ) ,
440+ ] )
441+ } finally {
442+ clearTimeout ( timer )
443+ }
444+ }
417445}
418446
419447function rethrowWithCapturedStackTrace ( constructorOpt : Function | undefined ) {
0 commit comments