11import {
22 type AnyNode ,
3+ type CanvasRootNode ,
34 type Collection ,
45 framer ,
56 isComponentNode ,
@@ -42,11 +43,12 @@ async function getNodeName(node: AnyNode): Promise<string | null> {
4243
4344export interface IndexerEvents extends EventMap {
4445 error : { error : Error }
45- progress : { processed : number ; total ?: number }
4646 started : { indexRun : number }
4747 completed : never
4848 restarted : never
4949 aborted : never
50+ canvasRootChangeStarted : never
51+ canvasRootChangeCompleted : never
5052}
5153
5254export class GlobalSearchIndexer {
@@ -58,6 +60,8 @@ export class GlobalSearchIndexer {
5860 // A smaller batch size will make showing results faster, but will also make the UI more laggy.
5961 private batchSize = 100
6062 private abortRequested = false
63+ private canvasSubscription : ( ( ) => void ) | null = null
64+ private currentCanvasRootChangeAbortController : AbortController | null = null
6165
6266 constructor ( private db : GlobalSearchDatabase ) { }
6367
@@ -165,35 +169,84 @@ export class GlobalSearchIndexer {
165169 }
166170 }
167171
172+ private async handleCanvasRootChange ( rootNode : CanvasRootNode ) {
173+ if ( this . abortRequested ) return
174+
175+ this . currentCanvasRootChangeAbortController ?. abort ( )
176+
177+ const abortController = new AbortController ( )
178+ this . currentCanvasRootChangeAbortController = abortController
179+
180+ this . eventEmitter . emit ( "canvasRootChangeStarted" )
181+
182+ try {
183+ if ( abortController . signal . aborted ) return
184+
185+ const lastIndexRun = await this . db . getLastIndexRun ( )
186+ const currentIndexRun = lastIndexRun + 1
187+ await this . processNodes ( currentIndexRun , [ rootNode ] , abortController . signal )
188+ await this . db . clearEntriesForRootNodeAndSpecificVersion ( rootNode . id , lastIndexRun )
189+ } catch ( error ) {
190+ this . eventEmitter . emit ( "error" , { error : error instanceof Error ? error : new Error ( String ( error ) ) } )
191+ } finally {
192+ if ( this . currentCanvasRootChangeAbortController === abortController ) {
193+ this . currentCanvasRootChangeAbortController = null
194+ }
195+ this . eventEmitter . emit ( "canvasRootChangeCompleted" )
196+ }
197+ }
198+
199+ private async processNodes (
200+ currentIndexRun : number ,
201+ rootNodes : readonly CanvasRootNode [ ] ,
202+ abortSignal ?: AbortSignal
203+ ) {
204+ const validRootNodes = rootNodes . filter ( rootNode => isComponentNode ( rootNode ) || isWebPageNode ( rootNode ) )
205+
206+ for await ( const batch of this . crawlNodes ( currentIndexRun , validRootNodes ) ) {
207+ if ( this . abortRequested || abortSignal ?. aborted ) break
208+ await this . db . upsertEntries ( batch )
209+ }
210+ }
211+
212+ private async processCollections ( currentIndexRun : number ) {
213+ const collections = await framer . getCollections ( )
214+
215+ for await ( const batch of this . crawlCollections ( currentIndexRun , collections ) ) {
216+ if ( this . abortRequested ) break
217+ await this . db . upsertEntries ( batch )
218+ }
219+ }
220+
168221 async start ( ) {
169222 // XXX: The indexer has no "locking mechanism" to prevent multiple instances from running at the same time in multiple tabs.
170223 try {
171224 const lastIndexRun = await this . db . getLastIndexRun ( )
172225 const currentIndexRun = lastIndexRun + 1
173226
174- const [ pages , components ] = await Promise . all ( [
227+ const [ pages , components , canvasRoot ] = await Promise . all ( [
175228 framer . getNodesWithType ( "WebPageNode" ) ,
176229 framer . getNodesWithType ( "ComponentNode" ) ,
230+ framer . getCanvasRoot ( ) ,
177231 ] )
178232
179233 this . abortRequested = false
180234 this . eventEmitter . emit ( "started" , { indexRun : currentIndexRun } )
181235
182- for await ( const batch of this . crawlNodes ( currentIndexRun , [ ...pages , ...components ] ) ) {
183- // this isn't a unnecassary static expression, as the value could change during the async loop
184- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
185- if ( this . abortRequested ) break
186- await this . db . upsertEntries ( batch )
187- }
236+ this . canvasSubscription ??= framer . subscribeToCanvasRoot ( rootNode => {
237+ void this . handleCanvasRootChange ( rootNode )
238+ } )
188239
189- const collections = await framer . getCollections ( )
240+ // Remove the current open canvas root from the list of root nodes to index
241+ // as it's already being indexed by the canvas root watcher
242+ const rootNodesWithoutCurrentRoot = [ ...pages , ...components ] . filter (
243+ rootNode => rootNode . id !== canvasRoot . id
244+ )
190245
191- for await ( const batch of this . crawlCollections ( currentIndexRun , collections ) ) {
192- // this isn't a unnecassary static expression, as the value could change during the async loop
193- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
194- if ( this . abortRequested ) break
195- await this . db . upsertEntries ( batch )
196- }
246+ await Promise . all ( [
247+ this . processNodes ( currentIndexRun , rootNodesWithoutCurrentRoot ) ,
248+ this . processCollections ( currentIndexRun ) ,
249+ ] )
197250
198251 // this isn't a unnecassary static expression, as the value could change during the async loop
199252 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -207,13 +260,24 @@ export class GlobalSearchIndexer {
207260 }
208261
209262 async restart ( ) {
210- this . abortRequested = true
263+ this . abort ( )
264+
211265 this . eventEmitter . emit ( "restarted" )
212266 return this . start ( )
213267 }
214268
215269 abort ( ) {
216270 this . abortRequested = true
271+
272+ this . currentCanvasRootChangeAbortController ?. abort ( )
273+ this . currentCanvasRootChangeAbortController = null
274+
275+ if ( this . canvasSubscription ) {
276+ this . canvasSubscription ( )
277+ this . canvasSubscription = null
278+ }
279+
280+ this . eventEmitter . emit ( "aborted" )
217281 }
218282
219283 /**
0 commit comments