* Sync with current file state (incremental update) * * Uses a mutex to prevent concurrent indexing operations.
(options: IndexOptions = {})
| 740 | * Uses a mutex to prevent concurrent indexing operations. |
| 741 | */ |
| 742 | async sync(options: IndexOptions = {}): Promise<SyncResult> { |
| 743 | return this.indexMutex.withLock(async () => { |
| 744 | try { |
| 745 | this.fileLock.acquire(); |
| 746 | } catch { |
| 747 | return { filesChecked: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, nodesUpdated: 0, durationMs: 0 }; |
| 748 | } |
| 749 | // Defer WAL auto-checkpointing for the whole incremental run, exactly |
| 750 | // as indexAll does for the bulk path (#1231): sync's store loop and its |
| 751 | // resolution passes churn the same FTS + secondary-index hot pages, and |
| 752 | // at the default 1000-page cadence the inline checkpoints re-write them |
| 753 | // over and over — on HDD-class storage a 7-file sync took 2 minutes at |
| 754 | // 0-2% CPU (#1248). The cost scales with the EXISTING database size, |
| 755 | // not the change size, so small syncs on big indexes hurt most. The |
| 756 | // valve bounds WAL growth off-thread; runMaintenance at the end does |
| 757 | // the final fold-up before the interval is restored in the finally. |
| 758 | // Same kill switch as indexAll: CODEGRAPH_NO_WAL_DEFER=1. Idle valve |
| 759 | // cost is one timer, so watcher-frequency syncs stay cheap. |
| 760 | const deferWal = process.env.CODEGRAPH_NO_WAL_DEFER !== '1' && this.db.getJournalMode() === 'wal'; |
| 761 | let walValve: WalCheckpointValve | null = null; |
| 762 | let priorAutocheckpoint = 1000; |
| 763 | if (deferWal) { |
| 764 | priorAutocheckpoint = this.db.getWalAutocheckpoint(); |
| 765 | this.db.setWalAutocheckpoint(0); |
| 766 | walValve = new WalCheckpointValve( |
| 767 | this.db, |
| 768 | resolveWalValveMb(process.env.CODEGRAPH_WAL_VALVE_MB, this.db.getDbFileSizeBytes()), |
| 769 | undefined, |
| 770 | options.verbose ? (m) => console.log(`[wal-valve] ${m}`) : undefined |
| 771 | ); |
| 772 | walValve.start(); |
| 773 | } |
| 774 | try { |
| 775 | // Captured BEFORE the sync runs: the sync's own incremental writes |
| 776 | // populate vocab rows for the files it touches, so an end-of-sync |
| 777 | // emptiness check would see "non-empty" and skip the backfill forever, |
| 778 | // leaving every unchanged file's names unsegmented. |
| 779 | const vocabWasEmpty = (() => { |
| 780 | try { return this.queries.isNameSegmentVocabEmpty(); } catch { return false; } |
| 781 | })(); |
| 782 | |
| 783 | const result = await this.orchestrator.sync(options.onProgress, options.paths); |
| 784 | |
| 785 | // Fold the store phase's WAL BEFORE the post-store reads below |
| 786 | // (resolution reads on the main thread) — same rationale as |
| 787 | // indexAll's fold between store and resolution. |
| 788 | if (walValve) await walValve.foldNow(); |
| 789 | |
| 790 | // Cross-file finalization (e.g. NestJS RouterModule prefixes). Run on |
| 791 | // every sync that touched files so edits to `app.module.ts` propagate |
| 792 | // to controllers in unchanged files. The pass is idempotent and cheap |
| 793 | // (regex over *.module.ts only). |
| 794 | if (result.filesAdded > 0 || result.filesModified > 0) { |
| 795 | this.resolver.runPostExtract(); |
| 796 | } else if (result.filesRemoved > 0) { |
| 797 | // A pure-removal sync still resolves refs below — the deletion path |
| 798 | // resurrects the removed file's incoming edges as pending refs |
| 799 | // (#1240 removal case) and the orphan sweep consumes them. In a |
no test coverage detected