Skip to content

Commit d5e363e

Browse files
Pause MD mirror during Keep and Obsidian imports
Each imported note fires a DB NOTIFY which would queue a reconcileOne write, flooding the mirror queue with hundreds of single-note file ops during a bulk import. pause() drops those; resume() runs one catch-up sweep afterward to mirror the entire batch cleanly in one pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 720625a commit d5e363e

3 files changed

Lines changed: 44 additions & 11 deletions

File tree

server/src/mirror/mirrorWorker.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,23 @@ let running = false;
517517
let lastSweep = null; // { at, summary } of the most recent real sweep
518518
let 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) {
537554
async 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() {
623641
async 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.
718739
async 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 };

server/src/routes/import-obsidian.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const os = require('os');
99
const unzipper = require('unzipper');
1010
const { processObsidianImport } = require('../import-obsidian');
1111
const { blockInDemo } = require('../middleware/demoGuard');
12+
const mirrorWorker = require('../mirror/mirrorWorker');
1213

1314
const tempDir = process.env.NODE_ENV === 'production'
1415
? '/tmp/itsnotes-obsidian'
@@ -155,12 +156,18 @@ router.post('/', blockInDemo, upload.fields([
155156
}
156157
};
157158

158-
const onStatus = (message) => send('status', { message });
159-
const result = await processObsidianImport(importFiles, resourceMap, onProgress, origNameMap, onStatus);
159+
mirrorWorker.pause('obsidian-import');
160+
let importResult;
161+
try {
162+
const onStatus = (message) => send('status', { message });
163+
importResult = await processObsidianImport(importFiles, resourceMap, onProgress, origNameMap, onStatus);
164+
} finally {
165+
mirrorWorker.resume();
166+
}
160167

161168
cleanup(...uploadedPaths, extractDir);
162169

163-
send('complete', { success: true, message: 'Import complete', result });
170+
send('complete', { success: true, message: 'Import complete', result: importResult });
164171
res.end();
165172
} catch (e) {
166173
console.error('Obsidian import route error:', e);

server/src/routes/import.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const os = require('os');
77
const unzipper = require('unzipper');
88
const { processGoogleKeepImport } = require('../import-notes');
99
const { blockInDemo } = require('../middleware/demoGuard');
10+
const mirrorWorker = require('../mirror/mirrorWorker');
1011

1112
// Define temp directory for Docker compatibility
1213
const tempDir = process.env.NODE_ENV === 'production'
@@ -206,7 +207,8 @@ router.post('/', blockInDemo, upload.single('archive'), async (req, res) => {
206207

207208
console.log(`Found Keep directory at ${keepDir}`);
208209
sendProgress('status', { message: 'Found Keep notes, starting import...' });
209-
210+
mirrorWorker.pause('keep-import');
211+
210212
// Process the import with progress updates
211213
try {
212214
// Patch the processGoogleKeepImport function to send progress
@@ -264,20 +266,23 @@ router.post('/', blockInDemo, upload.single('archive'), async (req, res) => {
264266

265267
const importResult = await processWithProgress(keepDir);
266268

269+
mirrorWorker.resume();
270+
267271
// Clean up temporary files
268272
cleanup(zipFilePath, extractionDir);
269-
273+
270274
// Send final success result
271-
sendProgress('complete', {
275+
sendProgress('complete', {
272276
success: true,
273277
message: 'Import completed successfully',
274-
result: importResult
278+
result: importResult
275279
});
276-
280+
277281
res.end();
278282
} catch (importError) {
283+
mirrorWorker.resume();
279284
console.error('Import process error:', importError);
280-
sendProgress('error', {
285+
sendProgress('error', {
281286
message: 'An error occurred during the import process',
282287
details: importError.message
283288
});

0 commit comments

Comments
 (0)