feat: 预览并恢复首次 Sync 合并且不丢失笔记链接

This commit is contained in:
2026-09-08 16:34:00 +08:00
parent f08a338ece
commit ef722145c4
19 changed files with 885 additions and 27 deletions
+46 -8
View File
@@ -136,7 +136,13 @@ impl Workspace {
.ok_or_else(|| HostError::new("SYNC_BINDING_CHANGED"))?;
revision.validate(&active)?;
self.resolve(&revision.path)?;
if revision.sequence != active.cursor + 1
let encoded =
serde_json::to_string(revision).map_err(|_| HostError::new("SYNC_RESPONSE_INVALID"))?;
if let Some(initial) = self.initial_revision(binding, revision.sequence)? {
if initial != encoded {
return Err(HostError::new("SYNC_REVISION_CHANGED"));
}
} else if revision.sequence != active.cursor + 1
|| self
.sync_boundary(binding)?
.is_none_or(|end| revision.sequence > end)
@@ -177,18 +183,38 @@ impl Workspace {
fn sync_finish(&mut self, binding: &str, revision: &RemoteRevision, state: &str) -> Result<()> {
self.check_binding(binding)?;
let tx = self.db.transaction()?;
let changed = tx.execute(
"UPDATE sync_bindings SET cursor=?2 WHERE id=?1 AND state='active' AND cursor=?3",
params![binding, revision.sequence, revision.sequence - 1],
)?;
if changed != 1 {
return Err(HostError::new("SYNC_CURSOR_INVALID"));
let initial: Option<i64> = tx
.query_row(
"SELECT boundary FROM sync_initial WHERE binding=?1",
[binding],
|r| r.get(0),
)
.optional()?;
if initial.is_none() {
let changed = tx.execute(
"UPDATE sync_bindings SET cursor=?2 WHERE id=?1 AND state='active' AND cursor=?3",
params![binding, revision.sequence, revision.sequence - 1],
)?;
if changed != 1 {
return Err(HostError::new("SYNC_CURSOR_INVALID"));
}
}
tx.execute("INSERT INTO sync_heads VALUES (?1,?2,?3,?4,?5) ON CONFLICT(binding,file_id) DO UPDATE SET revision=excluded.revision,path=excluded.path,hash=excluded.hash WHERE sync_heads.revision<excluded.revision", params![binding,revision.file_id,revision.sequence,revision.path,revision.hash.as_deref().unwrap_or("")])?;
tx.execute(
"UPDATE sync_inbox SET state=?3 WHERE binding=?1 AND sequence=?2",
params![binding, revision.sequence, state],
)?;
if let Some(boundary) = initial {
let pending:bool=tx.query_row("SELECT EXISTS(SELECT 1 FROM sync_initial_items i WHERE i.binding=?1 AND NOT EXISTS(SELECT 1 FROM sync_inbox n WHERE n.binding=i.binding AND n.sequence=i.sequence AND n.state!='pending'))",[binding],|r|r.get(0))?;
if !pending {
tx.execute(
"UPDATE sync_bindings SET cursor=?2 WHERE id=?1",
params![binding, boundary],
)?;
tx.execute("DELETE FROM sync_initial WHERE binding=?1", [binding])?;
tx.execute("DELETE FROM sync_initial_items WHERE binding=?1", [binding])?;
}
}
tx.execute(
"DELETE FROM sync_windows WHERE binding=?1 AND boundary=?2",
params![binding, revision.sequence],
@@ -223,7 +249,7 @@ impl Workspace {
remote
],
)?;
tx.execute("UPDATE sync_jobs SET state='conflict' WHERE binding=?1 AND file_id=?2 AND state NOT IN ('acked','archived')", params![binding,revision.file_id])?;
tx.execute("UPDATE sync_jobs SET state='conflict' WHERE binding=?1 AND (file_id=?2 OR path=?3) AND state NOT IN ('acked','archived')", params![binding,revision.file_id,local_path])?;
tx.commit()?;
self.sync_finish(binding, revision, "conflict")
}
@@ -255,6 +281,18 @@ impl Workspace {
self.sync_finish(binding, &revision, "applied")?;
return Ok(true);
}
let known: Option<i64> = self
.db
.query_row(
"SELECT revision FROM sync_heads WHERE binding=?1 AND file_id=?2",
params![binding, revision.file_id],
|r| r.get(0),
)
.optional()?;
if known.is_some_and(|head| head >= revision.sequence) {
self.sync_finish(binding, &revision, "applied")?;
return Ok(true);
}
let local_path = self.path_for_id(&revision.file_id).ok();
let path = local_path.as_deref().unwrap_or(&revision.path);
let local = self.resolve(path)?;