fix(canvas): 画布同步 - 避免版本冲突循环并合并重复提示

This commit is contained in:
ddCat
2026-09-12 00:08:57 +08:00
parent 96e0051a5c
commit ab58892586
4 changed files with 112 additions and 9 deletions

View File

@@ -29,6 +29,7 @@ let incrementalSession = false;
let sessionEpoch = 0;
const verifiedProjects = new Set<string>();
const verifiedAssets = new Set<string>();
const remoteProjectLoadPromises = new Map<string, Promise<CanvasProject | undefined>>();
export async function initializeRemoteUserDataSession(userId: string) {
await withRemoteUserDataSyncExclusive(async () => {
@@ -42,8 +43,10 @@ export async function initializeRemoteUserDataSession(userId: string) {
}
export async function loadCanvasProjectForEditing(id: string) {
const pending = remoteProjectLoadPromises.get(id);
if (pending) return pending;
const epoch = sessionEpoch;
return withRemoteUserDataSyncExclusive(async () => {
const request = withRemoteUserDataSyncExclusive(async () => {
if (epoch !== sessionEpoch) throw new Error("账号已切换,请重新打开画布");
const local = useCanvasStore.getState().projects.find((project) => project.id === id);
if (!activeRemoteUserId || verifiedProjects.has(id)) return local;
@@ -51,8 +54,10 @@ export async function loadCanvasProjectForEditing(id: string) {
await loadReferencedAssets(collectAssetIds(project));
const current = useCanvasStore.getState().projects.find((candidate) => candidate.id === id);
if (current && !sameEntitySnapshot(acknowledgedProjects.get(id), current)) {
const baseline = acknowledgedProjects.get(id);
if (baseline && Date.parse(baseline.updatedAt) !== Date.parse(project.updatedAt)) throw new Error("画布已在其他端修改,请先导出本地修改再重新加载");
// 当前页面已经开始编辑本地缓存时,远端详情只建立新的冲突基线;
// 保留当前画布内容,后续保存由当前打开的画布显式覆盖远端版本,
// 避免旧缓存被永久卡在“自动重试但永远冲突”的状态。
acknowledgedProjects.set(id, project);
verifiedProjects.add(id);
return current;
}
@@ -61,6 +66,17 @@ export async function loadCanvasProjectForEditing(id: string) {
useCanvasStore.setState((state) => ({ projects: [...state.projects.filter((candidate) => candidate.id !== id), project] }));
return project;
});
remoteProjectLoadPromises.set(id, request);
const clearPending = () => {
if (remoteProjectLoadPromises.get(id) === request) remoteProjectLoadPromises.delete(id);
};
void request.then(clearPending, clearPending);
return request;
}
async function waitForRemoteProjectLoads() {
const pending = [...remoteProjectLoadPromises.values()];
if (pending.length) await Promise.all(pending);
}
export async function loadAssetLibraryPage(options: Parameters<typeof listRemoteAssetsPage>[0]) {
@@ -169,6 +185,7 @@ export function resetRemoteUserDataSync() {
incrementalSession = false;
verifiedProjects.clear();
verifiedAssets.clear();
remoteProjectLoadPromises.clear();
activeRemoteUserId = "";
remoteUserDataPhase = "inactive";
acknowledgedAssets.clear();
@@ -376,6 +393,8 @@ export async function saveRemoteUserDataNow() {
const epoch = sessionEpoch;
if (!activeRemoteUserId) return;
requireRemoteUserDataBaseline();
// 画布先用本地缓存秒开时,远端详情校验可能仍在进行;写入必须等待校验结果。
await waitForRemoteProjectLoads();
if (syncPromise) {
syncQueued = true;
return syncPromise;
@@ -416,7 +435,11 @@ async function saveRemoteUserDataBatch(uploaded: Map<string, string>) {
const baseline = acknowledgedProjects.get(source.id);
if (!baseline || verifiedProjects.has(source.id)) continue;
const { project } = await getRemoteCanvasProject(source.id);
if (Date.parse(project.updatedAt) !== Date.parse(baseline.updatedAt)) throw new Error("画布远端版本已变化,已停止覆盖,请重新打开画布");
if (Date.parse(project.updatedAt) !== Date.parse(baseline.updatedAt)) {
// 以远端当前版本作为新的校验基线,继续提交当前打开画布的完整快照。
// 这是画布编辑态的显式覆盖策略,避免资产同步被旧缓存冲突永久阻塞。
acknowledgedProjects.set(source.id, project);
}
verifiedProjects.add(source.id);
}
for (const source of dirtyAssets) {