From d6a1ed817daf69bb656ba23bb30f24a08b32dc09 Mon Sep 17 00:00:00 2001 From: David Krcek Date: Wed, 10 Jun 2026 00:03:53 +0200 Subject: [PATCH] perf(sync): index notes by gitPath in pull classifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three loops each ran notes.find(n => n.gitPath === path) per remote file — O(remote x notes) per pass. Build a Map once (first-wins, preserving find's first-match semantics) and use O(1) lookups instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/utils/githubSync/syncPull.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/utils/githubSync/syncPull.ts b/src/utils/githubSync/syncPull.ts index fe8e9e0..3831211 100644 --- a/src/utils/githubSync/syncPull.ts +++ b/src/utils/githubSync/syncPull.ts @@ -148,6 +148,17 @@ export async function pullFromGitHub(input: { // paint.) const prefetchedBlobs = new Map() + // Index notes by gitPath ONCE so the per-remote-file lookups below are O(1) + // instead of O(notes). Three loops in this function used to each run + // notes.find(n => n.gitPath === path) per remote file — O(remote × notes). + // First-wins insertion preserves find()'s first-match semantics: the same + // gitPath can appear on both an active and a soft-deleted note, and the + // callers rely on getting the first array occurrence. + const notesByGitPath = new Map() + for (const n of notes) { + if (n.gitPath && !notesByGitPath.has(n.gitPath)) notesByGitPath.set(n.gitPath, n) + } + // 1. Walk every remote .md file. for (const [path, remoteSha] of remoteTree) { if (!path.endsWith('.md')) continue @@ -157,7 +168,7 @@ export async function pullFromGitHub(input: { // wants this gone — we MUST NOT treat the remote file as a new // creation and resurrect it. Push step 4 will emit the // `sha: null` tree entry to actually delete it. - let localMatch = notes.find(n => n.gitPath === path) + let localMatch = notesByGitPath.get(path) if (localMatch && localMatch.isDeleted) { // Pending deletion — skip the fetch + classification entirely. @@ -446,7 +457,7 @@ export async function pullFromGitHub(input: { const pendingRemovedPaths = new Set() for (const [path] of remoteTree) { if (!path.endsWith('.md')) continue - const localMatch = notes.find(n => n.gitPath === path) + const localMatch = notesByGitPath.get(path) if (!localMatch) continue if (localMatch.isDeleted) { pendingRemovedPaths.add(path) @@ -585,7 +596,7 @@ export async function pullFromGitHub(input: { for (const [path, remoteSha] of remoteTree) { if (!isForeignVaultFile(path)) continue if (gitignoreMatcher.isIgnored(path)) continue - const existing = notes.find(n => n.gitPath === path) + const existing = notesByGitPath.get(path) if (existing) continue out.push({ kind: 'foreignFile', path, remoteSha }) }