@@ -517,6 +517,23 @@ let running = false;
517517let lastSweep = null ; // { at, summary } of the most recent real sweep
518518let lastImport = null ; // { at, changed } of the most recent import that pulled anything in
519519
520+ // Import-in-progress gate. While a Keep or Obsidian import is running, each new
521+ // note fires a DB NOTIFY that would queue a reconcileOne — flooding the mirror
522+ // queue with hundreds of single-note writes. Pausing drops those during the
523+ // import; resume() runs one catch-up sweep afterward to mirror the whole batch.
524+ let importPaused = false ;
525+
526+ function pause ( label = 'import' ) {
527+ importPaused = true ;
528+ console . log ( `[md-mirror] paused (${ label } )` ) ;
529+ }
530+
531+ function resume ( ) {
532+ importPaused = false ;
533+ console . log ( '[md-mirror] resumed — scheduling catch-up sweep' ) ;
534+ runOnce ( ) . catch ( ( err ) => console . error ( '[md-mirror] catch-up sweep failed:' , err . message ) ) ;
535+ }
536+
520537// Every mirror mutation — the periodic sweep and each live single-note reconcile
521538// — runs through this one-at-a-time queue. They all compute a plan from a snapshot
522539// of the tracking table + folder and then write both back; if two ran at once the
@@ -537,6 +554,7 @@ function serialize(task) {
537554async function runOnce ( ) {
538555 const { enabled, root } = getConfig ( ) ;
539556 if ( ! enabled || ! root ) return { skipped : true , reason : 'disabled' } ;
557+ if ( importPaused ) return { skipped : true , reason : 'import-in-progress' } ;
540558 // A sweep already in flight (or queued behind live reconciles) will cover
541559 // whatever changed, so a second full sweep would just repeat the work — skip
542560 // it. Live reconciles still queue normally; they each target one note.
@@ -623,6 +641,9 @@ async function rebuild() {
623641async function reconcileOne ( noteId ) {
624642 const { enabled, root } = getConfig ( ) ;
625643 if ( ! enabled || ! root ) return { skipped : true , reason : 'disabled' } ;
644+ // Drop live reconciles during bulk imports — each imported note fires a NOTIFY
645+ // that would otherwise queue a single-note write. resume() runs a catch-up sweep.
646+ if ( importPaused ) return { skipped : true , reason : 'import-in-progress' } ;
626647 // No early-skip when busy: each reconcile targets a specific note and must run
627648 // to capture that note's latest state. The queue serializes it behind any
628649 // in-flight sweep or sibling reconcile so they never race on the same file.
@@ -717,7 +738,7 @@ let autoImporting = false;
717738// `autoImporting` guard only stops overlapping *ticks*, not the queue.
718739async function autoImportTick ( ) {
719740 const { enabled, root, autoImport } = getConfig ( ) ;
720- if ( ! enabled || ! root || ! autoImport || autoImporting ) return ;
741+ if ( ! enabled || ! root || ! autoImport || autoImporting || importPaused ) return ;
721742 autoImporting = true ;
722743 try {
723744 if ( ! ( await exists ( root ) ) ) return ;
@@ -829,4 +850,4 @@ async function processPendingDeletes(root) {
829850 }
830851}
831852
832- module . exports = { init, runOnce, rebuild, reconcileOne, getStatus, buildDesired, scanOnDisk, gatherDiskFiles, previewImport, applyImport, autoImportTick, broadcastImportResult } ;
853+ module . exports = { init, runOnce, rebuild, reconcileOne, getStatus, buildDesired, scanOnDisk, gatherDiskFiles, previewImport, applyImport, autoImportTick, broadcastImportResult, pause , resume } ;
0 commit comments