11/**
22 * Coordinator for multi-instance `<TimeToInitialDisplay>` / `<TimeToFullDisplay>`
33 * components on a single screen (active span).
4- *
5- * The aggregate "ready" state exposed via `isAllReady` is *deferred* on its
6- * way up: when the raw aggregate flips false→true, we schedule the public
7- * value to flip on the next macrotask. Down-flips (true→false) are
8- * immediate and cancel any pending up-flip.
9- *
10- * Why: in React 18, a typical "parent renders → parent useEffect setState
11- * → child mounts on next commit" wave executes synchronously inside one
12- * event-loop task. A `setTimeout(0)` reliably runs after that whole wave,
13- * so cross-commit-but-same-task peer registrations are absorbed before the
14- * coordinator declares itself ready. Without the defer, a header that
15- * registers and is alone-and-ready would emit `fullDisplay=true`
16- * immediately, the native reporter would fire on the next draw, and a
17- * sibling sidebar mounting on the next commit could only un-ready the
18- * aggregate after the (now stuck) native timestamp has already been
19- * recorded.
20- *
21- * The defer does NOT cover arbitrary-async deferred mounting (e.g. mount a
22- * checkpoint after a fetch resolves). That class of usage is documented
23- * against — the recommended pattern is to mount all checkpoints at screen
24- * mount with `ready=false` and flip them as data arrives.
254 */
265
276type Checkpoint = { ready : boolean } ;
@@ -30,36 +9,18 @@ type Listener = () => void;
309interface SpanRegistry {
3110 checkpoints : Map < string , Checkpoint > ;
3211 listeners : Set < Listener > ;
33- /**
34- * Stable, deferred view of the aggregate exposed via `isAllReady`. Lags
35- * raw `computeAggregate` on up-flips by `READY_DEFER_MS`, immediate on
36- * down-flips. Used to avoid waking subscribers when a checkpoint change
37- * does not flip the aggregate — the dominant lifecycle pattern is "all
38- * checkpoints register as not-ready, then flip to ready over time", and
39- * only the final flip needs to notify.
40- */
12+ // this value answers the question "are all checkpoints on this span ready?"
13+ // when the raw value goes from false to true, aggregateReady does NOT flip immediately, it gets
14+ // scheduled with setTimeout(0) in `reevaluate` function
15+ //
4116 aggregateReady : boolean ;
42- /**
43- * Pending up-flip timer. When non-null, an up-flip is scheduled but has
44- * not yet been applied to `aggregateReady`. Cleared either when the timer
45- * fires or when an intervening change cancels the pending up-flip.
46- */
17+ // when non-null, an up-flip is scheduled but has not yet been applied to `aggregateReady`
4718 pendingUpFlip : ReturnType < typeof setTimeout > | null ;
48- /**
49- * Checkpoint ids whose components have unmounted but are kept in the
50- * registry to prevent a premature aggregate flip (sole-blocker safeguard).
51- * They participate in `computeAggregate` but are excluded from the
52- * "live work" count in `performCleanup`.
53- */
19+ // `sticky` is used indicate checkpints that gets cleared when the screen fully unmounts
20+ // it's useful
5421 sticky : Set < string > ;
5522}
5623
57- /**
58- * Defer applied to up-flips. Zero macrotask is enough to absorb a same-task
59- * cascade of useEffect-driven child mounts in React 18.
60- */
61- const READY_DEFER_MS = 0 ;
62-
6324const TTID = 'ttid' ;
6425const TTFD = 'ttfd' ;
6526
@@ -105,17 +66,7 @@ function computeAggregate(entry: SpanRegistry): boolean {
10566 return true ;
10667}
10768
108- /**
109- * Recompute the raw aggregate and reconcile it with the cached
110- * `aggregateReady`. Up-flips are deferred to absorb same-task peer mounts;
111- * down-flips are immediate and cancel any pending up-flip.
112- *
113- * Transition matrix (raw, stable) → action:
114- * (false, false): no-op; cancel pending up-flip if any (it became stale).
115- * (true, true): no-op; cancel pending up-flip if any.
116- * (false, true): immediate down-flip + notify; cancel pending up-flip.
117- * (true, false): schedule up-flip if not already pending.
118- */
69+ // Recompute the raw aggregate and reconcile it with the cached `aggregateReady`
11970function reevaluate ( entry : SpanRegistry ) : void {
12071 const raw = computeAggregate ( entry ) ;
12172
@@ -131,10 +82,11 @@ function reevaluate(entry: SpanRegistry): void {
13182 return ;
13283 }
13384
134- // raw=true, stable=false: schedule deferred up-flip.
13585 if ( entry . pendingUpFlip !== null ) {
13686 return ;
13787 }
88+ // the delay here is set to 0 because in React 18 that
89+ // will schedule the callback to be run asynchronously after the shortest possible delay
13890 entry . pendingUpFlip = setTimeout ( ( ) => {
13991 entry . pendingUpFlip = null ;
14092 // Re-check on fire — a peer may have un-readied between schedule and now.
@@ -143,7 +95,7 @@ function reevaluate(entry: SpanRegistry): void {
14395 }
14496 entry . aggregateReady = true ;
14597 notifyListeners ( entry ) ;
146- } , READY_DEFER_MS ) ;
98+ } , 0 ) ;
14799}
148100
149101function notifyListeners ( entry : SpanRegistry ) : void {
@@ -152,16 +104,6 @@ function notifyListeners(entry: SpanRegistry): void {
152104 }
153105}
154106
155- /**
156- * Delete the registry entry once there is no live work attached to it.
157- *
158- * "Live work" means either subscribed listeners (registry-mode components
159- * still mounted) or non-sticky checkpoints (still-mounted registrations).
160- * Sticky checkpoints (kept after a not-ready unmount; see `unregister`)
161- * exist only to prevent premature aggregate flips while the screen is still
162- * mounted; once every live counterpart is gone, they are orphaned and safe
163- * to drop along with the entry.
164- */
165107function performCleanup ( kind : DisplayKind , parentSpanId : string , entry : SpanRegistry ) : void {
166108 const liveCheckpoints = entry . checkpoints . size - entry . sticky . size ;
167109 if ( liveCheckpoints === 0 && entry . listeners . size === 0 ) {
@@ -170,22 +112,16 @@ function performCleanup(kind: DisplayKind, parentSpanId: string, entry: SpanRegi
170112 }
171113}
172114
173- /**
174- * True iff removing `checkpointId` from `entry` would flip the aggregate
175- * from false to true — i.e., the checkpoint is the sole blocker.
176- *
177- * Used to detect the premature-fire scenario where a not-ready checkpoint
178- * unmounts while every other checkpoint is ready: deleting it would let the
179- * aggregate flip to true and immediately record TTFD/TTID, even though the
180- * unmounting source never actually became ready.
181- */
115+ // A bit of a hack but this is used to detect the premature-fire scenario
116+ // where a not-ready checkpoint unmounts while every other checkpoint is ready:
117+ // deleting it would let the aggregate flip to true and immediately record TTFD/TTID,
118+ // even though the unmounting source never actually became ready.
182119function isSoleBlocker ( entry : SpanRegistry , checkpointId : string ) : boolean {
183120 if ( entry . aggregateReady ) {
184121 return false ;
185122 }
186123 if ( entry . checkpoints . size <= 1 ) {
187- // Removing the only checkpoint leaves the registry empty, which yields
188- // aggregate=false (per `computeAggregate`). No flip.
124+ // because removing the only checkpoint leaves the registry empty
189125 return false ;
190126 }
191127 const cp = entry . checkpoints . get ( checkpointId ) ;
@@ -203,9 +139,6 @@ function isSoleBlocker(entry: SpanRegistry, checkpointId: string): boolean {
203139 return true ;
204140}
205141
206- /**
207- * Register a checkpoint under (kind, parentSpanId). Returns an unregister fn.
208- */
209142export function registerCheckpoint (
210143 kind : DisplayKind ,
211144 parentSpanId : string ,
@@ -221,10 +154,9 @@ export function registerCheckpoint(
221154 if ( ! e ) {
222155 return ;
223156 }
224- // If this checkpoint is the sole blocker, removing it would flip the
225- // aggregate to true and prematurely fire TTFD/TTID even though the
226- // unmounting source never became ready. Keep the checkpoint sticky;
227- // it gets cleared when the screen fully unmounts.
157+ // if the checkpoint is the only blocker then removing it would flip the
158+ // aggregate to true and fire TTFD/TTID even though the unmounting source never became ready.
159+ // that's why we use `sticky` here to indicate that it gets cleared when the screen fully unmounts
228160 if ( isSoleBlocker ( e , checkpointId ) ) {
229161 e . sticky . add ( checkpointId ) ;
230162 performCleanup ( kind , parentSpanId , e ) ;
@@ -238,9 +170,6 @@ export function registerCheckpoint(
238170 } ;
239171}
240172
241- /**
242- * Update an existing checkpoint's ready state.
243- */
244173export function updateCheckpoint (
245174 kind : DisplayKind ,
246175 parentSpanId : string ,
@@ -256,28 +185,19 @@ export function updateCheckpoint(
256185 reevaluate ( entry ) ;
257186}
258187
259- /**
260- * True if at least one checkpoint is registered AND all checkpoints are ready.
261- * Reads the cached aggregate — O(1).
262- */
188+ // Returns true if at least one checkpoint is registered AND all checkpoints are ready
263189export function isAllReady ( kind : DisplayKind , parentSpanId : string ) : boolean {
264190 const entry = registries [ kind ] . get ( parentSpanId ) ;
265191 return ! ! entry && entry . aggregateReady ;
266192}
267193
268- /**
269- * Returns true if there is at least one registered checkpoint on this span.
270- */
194+ // Returns true if there is at least one registered checkpoint on this span
271195export function hasAnyCheckpoints ( kind : DisplayKind , parentSpanId : string ) : boolean {
272196 const entry = registries [ kind ] . get ( parentSpanId ) ;
273197 return ! ! entry && entry . checkpoints . size > 0 ;
274198}
275199
276- /**
277- * Subscribe to aggregate-ready transitions for a given span. The listener is
278- * called only when the aggregate flips, not on every individual checkpoint
279- * change.
280- */
200+ // Subscribe to aggregate-ready transitions for a given span
281201export function subscribe ( kind : DisplayKind , parentSpanId : string , listener : Listener ) : ( ) => void {
282202 const entry = getOrCreate ( kind , parentSpanId ) ;
283203 entry . listeners . add ( listener ) ;
@@ -291,21 +211,10 @@ export function subscribe(kind: DisplayKind, parentSpanId: string, listener: Lis
291211 } ;
292212}
293213
294- /**
295- * Drop coordinator state for `parentSpanId` across both kinds.
296- *
297- * Called by the time-to-display integration once a transaction has been
298- * processed, since the per-span coordinator state is no longer relevant
299- * after the native draw timestamps have been read. Without this hook,
300- * entries for screens that stay mounted past the end of their span
301- * (React Navigation keep-alive, idle-timeout discarded transactions,
302- * etc.) would accumulate in the module-level registries.
303- *
304- * Components that are still subscribed when their span is cleared remain
305- * functional: their next interaction recreates the entry under the same
306- * (now stale) parentSpanId. Since the integration has already read the
307- * native ttid/ttfd values for that span, any subsequent fires are inert.
308- */
214+ // Drop coordinator state for `parentSpanId` across both kinds.
215+ // Called by the time-to-display integration once a transaction has been
216+ // processed, since the per-span coordinator state is no longer relevant
217+ // after the native draw timestamps have been read.
309218export function clearSpan ( parentSpanId : string ) : void {
310219 for ( const kind of [ TTID , TTFD ] as const ) {
311220 const entry = registries [ kind ] . get ( parentSpanId ) ;
@@ -316,9 +225,6 @@ export function clearSpan(parentSpanId: string): void {
316225 }
317226}
318227
319- /**
320- * Test-only. Clears all coordinator state.
321- */
322228export function _resetTimeToDisplayCoordinator ( ) : void {
323229 for ( const kind of [ TTID , TTFD ] as const ) {
324230 for ( const entry of registries [ kind ] . values ( ) ) {
0 commit comments