Skip to content

Commit faa74f4

Browse files
committed
implementation
1 parent 58af936 commit faa74f4

1 file changed

Lines changed: 53 additions & 34 deletions

File tree

‎src/renderStream/createRenderStream.tsx‎

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {type RenderStreamContextValue} from './context.js'
1111
import {RenderStreamContextProvider} from './context.js'
1212
import {syncQueries, type Queries, type SyncQueries} from './syncQueries.js'
1313

14+
let nextProfilerId = 0
15+
1416
export 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

Comments
 (0)