33 reindexSnapshotNodes ,
44 type SnapshotScopeCandidate ,
55} from '@agent-device/contracts/snapshot' ;
6+ import type { AndroidSnapshotPresentationBudget } from './snapshot-presentation.ts' ;
67
78type PresentedNode = { index : number ; depth ?: number ; parentIndex ?: number } ;
89type SourceNode = SnapshotScopeCandidate & { depth : number ; children : SourceNode [ ] } ;
@@ -46,21 +47,33 @@ export function scopePresentedAndroidSnapshot<
4647 roots : readonly Source [ ] ,
4748 scope : string ,
4849 maxDepth : number ,
50+ budget : AndroidSnapshotPresentationBudget ,
4951) : AndroidPresentedNodes < Node , Source > {
5052 const presented = new Set < Source > ( state . sourceNodes ) ;
51- const scopeRoot = findAndroidScopeRoot ( roots , scope , presented ) ;
53+ const order = collectDocumentOrder ( roots , budget ) ;
54+ const contributing = collectPresentedSubtrees ( order , presented , budget ) ;
55+ const scopeRoot = findAndroidScopeRoot ( order , scope , contributing , budget ) ;
5256 if ( ! scopeRoot ) return { nodes : [ ] , sourceNodes : [ ] } ;
5357
54- const inScope = collectSubtree ( scopeRoot ) ;
55- const subtree = [ ...state . sourceNodes . entries ( ) ]
56- . filter ( ( [ , source ] ) => inScope . has ( source ) )
57- . map ( ( [ position ] ) => position ) ;
58+ const inScope = new Set ( collectDocumentOrder ( [ scopeRoot ] , budget ) ) ;
59+ const subtree : number [ ] = [ ] ;
60+ for ( const [ position , source ] of state . sourceNodes . entries ( ) ) {
61+ budget . check ( 'work' ) ;
62+ if ( inScope . has ( source ) ) subtree . push ( position ) ;
63+ }
5864 if ( subtree . length === 0 ) return { nodes : [ ] , sourceNodes : [ ] } ;
5965
60- const depthOffset = Math . min ( ...subtree . map ( ( position ) => state . nodes [ position ] ?. depth ?? 0 ) ) ;
61- const positions = subtree . filter (
62- ( position ) => ( state . nodes [ position ] ?. depth ?? 0 ) - depthOffset <= maxDepth ,
63- ) ;
66+ let depthOffset = Number . POSITIVE_INFINITY ;
67+ for ( const position of subtree ) {
68+ budget . check ( 'work' ) ;
69+ depthOffset = Math . min ( depthOffset , state . nodes [ position ] ?. depth ?? 0 ) ;
70+ }
71+ const positions : number [ ] = [ ] ;
72+ for ( const position of subtree ) {
73+ budget . check ( 'work' ) ;
74+ if ( ( state . nodes [ position ] ?. depth ?? 0 ) - depthOffset <= maxDepth ) positions . push ( position ) ;
75+ }
76+ budget . consume ( positions . length ) ;
6477 return {
6578 nodes : reindexSnapshotNodes (
6679 positions . map ( ( position ) => state . nodes [ position ] as Node ) ,
@@ -72,37 +85,56 @@ export function scopePresentedAndroidSnapshot<
7285
7386/** First document-order match whose subtree contributes presented content. */
7487function findAndroidScopeRoot < Source extends SourceNode > (
75- roots : readonly Source [ ] ,
88+ order : readonly Source [ ] ,
7689 scope : string ,
77- presented : ReadonlySet < Source > ,
90+ contributing : ReadonlySet < Source > ,
91+ budget : AndroidSnapshotPresentationBudget ,
7892) : Source | null {
79- for ( const node of documentOrder ( roots ) ) {
80- if ( matchesSnapshotScope ( node , scope ) && subtreeHasPresentedNode ( node , presented ) ) return node ;
93+ for ( const node of order ) {
94+ budget . check ( 'work' ) ;
95+ if ( matchesSnapshotScope ( node , scope ) && contributing . has ( node ) ) return node ;
8196 }
8297 return null ;
8398}
8499
85- function * documentOrder < Source extends SourceNode > ( roots : readonly Source [ ] ) : Generator < Source > {
100+ function collectDocumentOrder < Source extends SourceNode > (
101+ roots : readonly Source [ ] ,
102+ budget : AndroidSnapshotPresentationBudget ,
103+ ) : Source [ ] {
104+ const order : Source [ ] = [ ] ;
86105 const stack = [ ...roots ] . reverse ( ) ;
87106 while ( stack . length > 0 ) {
107+ budget . check ( 'work' ) ;
88108 const node = stack . pop ( ) as Source ;
89- yield node ;
109+ order . push ( node ) ;
90110 for ( let index = node . children . length - 1 ; index >= 0 ; index -= 1 ) {
111+ budget . check ( 'work' ) ;
91112 stack . push ( node . children [ index ] as Source ) ;
92113 }
93114 }
115+ return order ;
94116}
95117
96- function subtreeHasPresentedNode < Source extends SourceNode > (
97- root : Source ,
118+ function collectPresentedSubtrees < Source extends SourceNode > (
119+ order : readonly Source [ ] ,
98120 presented : ReadonlySet < Source > ,
99- ) : boolean {
100- for ( const node of documentOrder ( [ root ] ) ) {
101- if ( presented . has ( node ) ) return true ;
121+ budget : AndroidSnapshotPresentationBudget ,
122+ ) : ReadonlySet < Source > {
123+ const contributing = new Set < Source > ( ) ;
124+ for ( let index = order . length - 1 ; index >= 0 ; index -= 1 ) {
125+ budget . check ( 'work' ) ;
126+ const node = order [ index ] as Source ;
127+ if ( presented . has ( node ) ) {
128+ contributing . add ( node ) ;
129+ continue ;
130+ }
131+ for ( const child of node . children ) {
132+ budget . check ( 'work' ) ;
133+ if ( contributing . has ( child as Source ) ) {
134+ contributing . add ( node ) ;
135+ break ;
136+ }
137+ }
102138 }
103- return false ;
104- }
105-
106- function collectSubtree < Source extends SourceNode > ( root : Source ) : ReadonlySet < Source > {
107- return new Set ( documentOrder ( [ root ] ) ) ;
139+ return contributing ;
108140}
0 commit comments