SyncClient.runSync({ automatic, protectedPaths })
│
├─ 1. await this.initialize()
│ └─ LocalSyncClientMethods.initialize()
│ ├─ fs.realpath(rootPath)
│ ├─ fs.stat(rootPath)
│ ├─ localLibraryName(rootPath)
│ ├─ ensureSafeMetadataDirectory(metadataPath, stateNamespace)
│ ├─ loadOrCreateIdentity(library.json, 'libraryId')
│ ├─ loadOrCreateIdentity(device.json, 'deviceId')
│ ├─ new SyncStateStore(state.json, { libraryId, deviceId })
│ │ └─ store.load()
│ │ ├─ fs.lstat(stateFile)
│ │ ├─ JSON.parse(fs.readFile(stateFile))
│ │ ├─ logStore.recent(200)
│ │ └─ 若 status === 'syncing' → 标记 error + save()
│ ├─ new SyncHistoryStore(history-v1, { libraryId })
│ ├─ new ManagedPathStore(rootPath)
│ └─ managedPathStore.add(已有 state.files + state.queue 路径)
│
├─ 2. this.beginSyncDiagnostic(state, automatic ? 'auto' : 'manual')
│ └─ SyncDiagnosticMethods.beginSyncDiagnostic()
│ ├─ 构造 activeSyncDiagnostic
│ └─ store.log('info', ...)
│
├─ 3. await this.unresolvedSyncConflictPaths()
│ └─ LocalSyncClientMethods.unresolvedSyncConflictPaths()
│ ├─ this.scanFiles({ recordManagedPaths: false })
│ │ ├─ this.initialize()
│ │ ├─ loadMyNoteIgnore(rootPath)
│ │ ├─ managedPathStore.paths()
│ │ ├─ 递归 walk(rootPath)
│ │ │ ├─ fs.readdir
│ │ │ ├─ ignorePolicy.ignores()
│ │ │ ├─ contentEncoding(relativePath)
│ │ │ ├─ fs.readFile + fs.stat
│ │ │ └─ contentHash(content, encoding)
│ │ └─ managedPathStore.add(files.keys())
│ └─ 对每个 .md 检查 syncConflictRelativePaths()
│ ├─ .base
│ ├─ .local
│ └─ .remote
│
├─ 4. 若存在冲突:
│ ├─ await this.logUnresolvedSyncConflicts(conflictPaths)
│ │ └─ SyncDiagnosticMethods.logUnresolvedSyncConflicts()
│ │ └─ this.describeUnresolvedSyncConflict()
│ │ ├─ fs.readFile(original/.base/.local/.remote)
│ │ └─ contentHash()
│ ├─ await this.store.save()
│ ├─ this.activeSyncDiagnostic = null
│ └─ throw new Error(...)
│
├─ 5. state.status = 'syncing'
├─ 6. state.lastError = null
├─ 7. this.setProgress({ phase: 'starting', ... })
│ └─ LocalSyncClientMethods.setProgress()
│ ├─ store.state.progress = value
│ └─ 通知 progressListeners
│
├─ 8. await this.store.save()
│ └─ SyncStateStore.save()
│ ├─ flush pendingLogs → SyncLogStore.append()
│ └─ atomicWriteJson(state.json)
│
├─ 9. await this.ensureFileModifiedTimes(state)
│ └─ SyncTransferMethods.ensureFileModifiedTimes()
│ ├─ 若 state.fileHistoryTimeVersion === 1 → return
│ ├─ remote.pull(libraryId, 0)
│ ├─ this.observeRemoteVersion(response)
│ │ └─ SyncDiagnosticMethods.observeRemoteVersion()
│ ├─ 用 response.changes 补齐 tracked.modifiedAt
│ ├─ state.fileHistoryTimeVersion = 1
│ └─ await this.store.save()
│
├─ 10. 若 protectedPaths 非空:
│ ├─ remote.pull(libraryId, state.cursor)
│ ├─ this.remoteChangeTouchesProtectedPath(
│ │ response.changes,
│ │ state,
│ │ protectedPaths
│ │ )
│ │ └─ SyncAutomaticMethods.remoteChangeTouchesProtectedPath()
│ │ ├─ currentProtectedPaths(protectedPaths)
│ │ └─ 检查 change.relativePath / previousPath
│ └─ 若命中 → throw SafeSyncBlockedError('dirty-open-document')
│
├─ 11. 若 state.cursor === 0
│ && state.queue.length === 0
│ && Object.keys(state.files).length === 0:
│ └─ await this.pullChanges({ protectedPaths })
│ └─ SyncTransferMethods.pullChanges()
│ ├─ remote.pull(libraryId, startingCursor)
│ ├─ observeRemoteVersion()
│ ├─ 合并 finalByFileId
│ ├─ 排序 changes
│ ├─ remoteChangeTouchesProtectedPath()
│ ├─ 预读 localSnapshots
│ │ └─ this.readSyncFile(tracked.relativePath)
│ │ └─ SyncFileMethods.readSyncFile()
│ │ ├─ assertSafeTarget(relativePath)
│ │ ├─ fs.readFile
│ │ └─ contentHash()
│ ├─ 对每个 change:
│ │ └─ this.applyRemoteFile(change, {
│ │ compacted: true,
│ │ localFileSnapshot,
│ │ batchSourcePaths,
│ │ safe
│ │ })
│ │ └─ SyncFileMethods.applyRemoteFile()
│ │ ├─ state.files[fileId]
│ │ ├─ readSyncFile()
│ │ ├─ tryAutoMerge()
│ │ │ └─ SyncMergeMethods.tryAutoMerge()
│ │ │ ├─ loadMergeBase()
│ │ │ │ └─ historyStore.versionDetails() / remote.historyPage()
│ │ │ ├─ hasConflictSidecars()
│ │ │ ├─ writeConflictSidecars()
│ │ │ │ └─ atomicWriteBuffer(.base/.local/.remote)
│ │ │ └─ mergeText()
│ │ ├─ preserveConflict()
│ │ ├─ moveToSyncTrash()
│ │ │ ├─ fs.rename → .mynote/sync-trash
│ │ │ └─ removePublishedMarkdown()
│ │ ├─ writeFile()
│ │ │ ├─ assertSafeTarget()
│ │ │ ├─ fs.mkdir
│ │ │ └─ atomicWriteBuffer()
│ │ ├─ state.files[fileId] = publicFileRecord()
│ │ └─ managedPathStore.add/move/remove
│ ├─ managedPathStore.replace(...)
│ ├─ state.cursor = response.cursor
│ ├─ await this.store.save()
│ └─ await this.cleanupMergeBasesAfterCheckpoint()
│
├─ 12. 两轮同步循环:
│ for (let pass = 0; pass < 2; pass += 1) {
│ await this.reconcileLocal();
│ await this.pushQueue();
│ await this.pullChanges({ protectedPaths });
│ }
│
│ ├─ 12.1 await this.reconcileLocal()
│ │ └─ LocalSyncClientMethods.reconcileLocal()
│ │ ├─ this.initialize()
│ │ ├─ loadMyNoteIgnore(rootPath)
│ │ ├─ 清理 isAlwaysExcludedPath 的 state.files / state.queue
│ │ │ └─ managedPathStore.remove()
│ │ ├─ 若 state.queue.length → return state.queue.length
│ │ ├─ this.setProgress({ phase: 'scanning', ... })
│ │ ├─ managedPathStore.paths()
│ │ ├─ 把 state.files / state.queue 路径加入 managedPaths
│ │ ├─ this.scanFiles({ reportProgress: true, ignorePolicy, managedPaths })
│ │ │ ├─ 递归 walk
│ │ │ │ ├─ fs.readdir
│ │ │ │ ├─ ignorePolicy.ignores()
│ │ │ │ ├─ contentEncoding()
│ │ │ │ ├─ fs.readFile + fs.stat
│ │ │ │ └─ contentHash()
│ │ │ └─ managedPathStore.add(files.keys())
│ │ ├─ 若 checkRemotePaths:
│ │ │ └─ this.reconcileRemotePathOwners(state, diskFiles)
│ │ │ ├─ this.remotePathOwners(untrackedPaths)
│ │ │ │ └─ remote.pull(libraryId, 0)
│ │ │ └─ 对每个 owner:
│ │ │ ├─ 若 hash 相同 → 采纳 remote fileId
│ │ │ │ ├─ state.files[fileId] = publicFileRecord()
│ │ │ │ └─ managedPathStore.add()
│ │ │ └─ 否则 → this.applyRemoteFile(remoteFile)
│ │ ├─ this.setProgress({ phase: 'reconciling', ... })
│ │ ├─ 比较 activeFiles vs diskFiles
│ │ │ ├─ 内容变化 → this.operation('upsert', file, disk)
│ │ │ ├─ 唯一同内容新路径 → this.operation('rename', file, { relativePath })
│ │ │ ├─ 消失且无重命名候选 → this.operation('delete', file, { relativePath: null })
│ │ │ └─ 新路径 → this.operation('upsert', { fileId: uuid, revision: 0 }, disk)
│ │ │ └─ LocalSyncClientMethods.operation()
│ │ │ ├─ crypto.randomUUID()
│ │ │ ├─ PROTOCOL_VERSION
│ │ │ ├─ contentEncoding(relativePath)
│ │ │ └─ 构造 Operation
│ │ ├─ state.queue.push(...queued)
│ │ ├─ store.log('info', ...)
│ │ └─ await this.store.save()
│ │
│ ├─ 12.2 await this.pushQueue()
│ │ └─ SyncTransferMethods.pushQueue()
│ │ ├─ state.queue[0]
│ │ ├─ this.setProgress({ phase: 'uploading', ... })
│ │ ├─ await this.remote.applyOperation(operation)
│ │ │ └─ HostedSyncRemote.applyOperation()
│ │ │ └─ this.request('POST', `/v1/libraries/${libraryId}/operations`, { body: operation })
│ │ │ └─ HostedSyncRemote.request()
│ │ │ ├─ AbortController + timeout
│ │ │ ├─ fetchImpl(endpoint + pathname, { method, headers, body, signal })
│ │ │ ├─ response.json()
│ │ │ └─ 若 !response.ok → 抛出 HostedRequestError
│ │ ├─ 若 result.status === 'accepted':
│ │ │ └─ state.files[operation.fileId] = publicFileRecord(result.file)
│ │ ├─ 若 result.status === 'conflict':
│ │ │ ├─ 若 safe → 记录日志、save、cleanupMergeBasesAfterCheckpoint、return false
│ │ │ └─ 否则 this.resolvePushConflict(operation, result)
│ │ │ └─ SyncFileMethods.resolvePushConflict()
│ │ │ ├─ state.files[fileId]
│ │ │ ├─ 判断 equivalentPathOwner
│ │ │ ├─ this.readSyncFile()
│ │ │ ├─ this.tryAutoMerge()
│ │ │ │ └─ SyncMergeMethods.tryAutoMerge()
│ │ │ ├─ this.applyRemoteFile()
│ │ │ ├─ this.preserveConflict()
│ │ │ ├─ this.moveToSyncTrash()
│ │ │ └─ 返回 { rebasedOperation, cleanupConflictPath }
│ │ ├─ 若 conflictResolution.rebasedOperation:
│ │ │ ├─ state.queue[0] = rebasedOperation
│ │ │ ├─ await this.store.save()
│ │ │ ├─ await this.removeConflictSidecars(...)
│ │ │ └─ continue
│ │ ├─ state.queue.shift()
│ │ ├─ this.setProgress(...)
│ │ ├─ await this.store.save()
│ │ ├─ 若 deduplicated && cleanupConflictPath:
│ │ │ └─ this.removeConflictSidecars()
│ │ ├─ 若 result.status === 'accepted':
│ │ │ ├─ 删除 → managedPathStore.remove()
│ │ │ ├─ rename → managedPathStore.move()
│ │ │ └─ 其他 → managedPathStore.add()
│ │ └─ 循环结束:await this.cleanupMergeBasesAfterCheckpoint()
│ │
│ └─ 12.3 await this.pullChanges({ protectedPaths })
│ └─ 同 11,但 providedResponse 可能为空,重新 remote.pull()
│
├─ 13. await syncVersionHistory(this)
│ └─ src/main/sync/history-store.ts
│ ├─ client.initialize()
│ ├─ client.historyStore.contiguousRevision()
│ ├─ 若 cachedRevision === throughRevision → return
│ ├─ client.setProgress({ phase: 'history', ... })
│ ├─ 循环:
│ │ ├─ client.remote.historyPage(libraryId, afterRevision, throughRevision, 100)
│ │ │ └─ HostedSyncRemote.historyPage()
│ │ │ └─ this.request('GET', `/v1/libraries/${libraryId}/versions?...`)
│ │ ├─ 校验 response
│ │ ├─ rebuilding ? replacementDetails.push(...) : client.historyStore.appendContiguous(...)
│ │ │ ├─ atomicWriteJson(versions/{revision}.json)
│ │ │ └─ atomicWriteJson(manifest.json)
│ │ └─ client.setProgress(...)
│ └─ 若 rebuilding → client.historyStore.replace(replacementDetails)
│ ├─ atomicWriteJson(versions/{revision}.json)
│ └─ atomicWriteJson(manifest.json)
│
├─ 14. state.status = 'idle'
├─ 15. this.setProgress(null)
├─ 16. state.lastSyncedAt = this.now().toISOString()
├─ 17. this.store.log('info', '同步完成。', this.syncVersionDetails(state))
│ └─ SyncDiagnosticMethods.syncVersionDetails()
├─ 18. await this.store.save()
│ └─ SyncStateStore.save()
├─ 19. return this.status()
│ └─ SyncTransferMethods.status()
│ ├─ this.initialize()
│ └─ 返回 protocolVersion / libraryId / deviceId / status / progress / pendingCount / cursor / lastError / lastSyncedAt / logs
│
└─ catch (error)
├─ 若 error instanceof SafeSyncBlockedError && (automatic || reason === 'dirty-open-document'):
│ ├─ state.status = 'idle'
│ ├─ this.setProgress(null)
│ ├─ state.lastError = null
│ ├─ this.logSyncOutcome('info', '同步已暂缓...', state, { reason })
│ ├─ await this.store.save()
│ └─ return { ...status(), syncSkipped: true, automaticSync: 'skipped', reason }
├─ 否则:
│ ├─ state.status = 'error'
│ ├─ this.setProgress(null)
│ ├─ state.lastError = error.message
│ ├─ this.logSyncOutcome('error', '同步失败...', state, { message })
│ ├─ await this.store.save()
│ └─ throw error
└─ finally
└─ this.activeSyncDiagnostic = null
目录
标题
