-
Notifications
You must be signed in to change notification settings - Fork 49
Global search: Indexer watches for change #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70b4274
fb6a6f0
eab9b63
0640f46
ef07fdf
5595762
9daf2e3
02a8e04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||
| import { | ||||||||||||||||
| type AnyNode, | ||||||||||||||||
| type CanvasRootNode, | ||||||||||||||||
| type Collection, | ||||||||||||||||
| framer, | ||||||||||||||||
| isComponentNode, | ||||||||||||||||
|
|
@@ -42,11 +43,12 @@ async function getNodeName(node: AnyNode): Promise<string | null> { | |||||||||||||||
|
|
||||||||||||||||
| export interface IndexerEvents extends EventMap { | ||||||||||||||||
| error: { error: Error } | ||||||||||||||||
| progress: { processed: number; total?: number } | ||||||||||||||||
| started: { indexRun: number } | ||||||||||||||||
| completed: never | ||||||||||||||||
| restarted: never | ||||||||||||||||
| aborted: never | ||||||||||||||||
| canvasRootChangeStarted: never | ||||||||||||||||
| canvasRootChangeCompleted: never | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export class GlobalSearchIndexer { | ||||||||||||||||
|
|
@@ -58,6 +60,8 @@ export class GlobalSearchIndexer { | |||||||||||||||
| // A smaller batch size will make showing results faster, but will also make the UI more laggy. | ||||||||||||||||
| private batchSize = 100 | ||||||||||||||||
| private abortRequested = false | ||||||||||||||||
| private canvasSubscription: (() => void) | null = null | ||||||||||||||||
| private currentCanvasRootChangeAbortController: AbortController | null = null | ||||||||||||||||
|
|
||||||||||||||||
| constructor(private db: GlobalSearchDatabase) {} | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -165,35 +169,84 @@ export class GlobalSearchIndexer { | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private async handleCanvasRootChange(rootNode: CanvasRootNode) { | ||||||||||||||||
| if (this.abortRequested) return | ||||||||||||||||
|
|
||||||||||||||||
| this.currentCanvasRootChangeAbortController?.abort() | ||||||||||||||||
|
|
||||||||||||||||
| const abortController = new AbortController() | ||||||||||||||||
| this.currentCanvasRootChangeAbortController = abortController | ||||||||||||||||
|
|
||||||||||||||||
| this.eventEmitter.emit("canvasRootChangeStarted") | ||||||||||||||||
|
|
||||||||||||||||
| try { | ||||||||||||||||
| if (abortController.signal.aborted) return | ||||||||||||||||
|
|
||||||||||||||||
| const lastIndexRun = await this.db.getLastIndexRun() | ||||||||||||||||
| const currentIndexRun = lastIndexRun + 1 | ||||||||||||||||
| await this.processNodes(currentIndexRun, [rootNode], abortController.signal) | ||||||||||||||||
| await this.db.clearEntriesForRootNodeAndSpecificVersion(rootNode.id, lastIndexRun) | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| this.eventEmitter.emit("error", { error: error instanceof Error ? error : new Error(String(error)) }) | ||||||||||||||||
| } finally { | ||||||||||||||||
| if (this.currentCanvasRootChangeAbortController === abortController) { | ||||||||||||||||
| this.currentCanvasRootChangeAbortController = null | ||||||||||||||||
| } | ||||||||||||||||
| this.eventEmitter.emit("canvasRootChangeCompleted") | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private async processNodes( | ||||||||||||||||
| currentIndexRun: number, | ||||||||||||||||
| rootNodes: readonly CanvasRootNode[], | ||||||||||||||||
| abortSignal?: AbortSignal | ||||||||||||||||
| ) { | ||||||||||||||||
| const validRootNodes = rootNodes.filter(rootNode => isComponentNode(rootNode) || isWebPageNode(rootNode)) | ||||||||||||||||
|
elmarburke marked this conversation as resolved.
|
||||||||||||||||
|
|
||||||||||||||||
| for await (const batch of this.crawlNodes(currentIndexRun, validRootNodes)) { | ||||||||||||||||
| if (this.abortRequested || abortSignal?.aborted) break | ||||||||||||||||
| await this.db.upsertEntries(batch) | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private async processCollections(currentIndexRun: number) { | ||||||||||||||||
| const collections = await framer.getCollections() | ||||||||||||||||
|
|
||||||||||||||||
| for await (const batch of this.crawlCollections(currentIndexRun, collections)) { | ||||||||||||||||
| if (this.abortRequested) break | ||||||||||||||||
| await this.db.upsertEntries(batch) | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async start() { | ||||||||||||||||
| // XXX: The indexer has no "locking mechanism" to prevent multiple instances from running at the same time in multiple tabs. | ||||||||||||||||
| try { | ||||||||||||||||
| const lastIndexRun = await this.db.getLastIndexRun() | ||||||||||||||||
| const currentIndexRun = lastIndexRun + 1 | ||||||||||||||||
|
|
||||||||||||||||
| const [pages, components] = await Promise.all([ | ||||||||||||||||
| const [pages, components, canvasRoot] = await Promise.all([ | ||||||||||||||||
| framer.getNodesWithType("WebPageNode"), | ||||||||||||||||
| framer.getNodesWithType("ComponentNode"), | ||||||||||||||||
| framer.getCanvasRoot(), | ||||||||||||||||
| ]) | ||||||||||||||||
|
|
||||||||||||||||
| this.abortRequested = false | ||||||||||||||||
| this.eventEmitter.emit("started", { indexRun: currentIndexRun }) | ||||||||||||||||
|
|
||||||||||||||||
| for await (const batch of this.crawlNodes(currentIndexRun, [...pages, ...components])) { | ||||||||||||||||
| // this isn't a unnecassary static expression, as the value could change during the async loop | ||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||||||||||||||||
| if (this.abortRequested) break | ||||||||||||||||
| await this.db.upsertEntries(batch) | ||||||||||||||||
| } | ||||||||||||||||
| this.canvasSubscription ??= framer.subscribeToCanvasRoot(rootNode => { | ||||||||||||||||
|
||||||||||||||||
| this.canvasSubscription ??= framer.subscribeToCanvasRoot(rootNode => { | |
| // Always clean up any existing canvas subscription before creating a new one | |
| if (this.canvasSubscription) { | |
| this.canvasSubscription(); | |
| this.canvasSubscription = null; | |
| } | |
| this.canvasSubscription = framer.subscribeToCanvasRoot(rootNode => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with the nullish coalescing assignment (what a word to type) there should only be ever one around, when it starts again, the active one is good to be re-used.
Uh oh!
There was an error while loading. Please reload this page.