fix: retain staged images while editor drafts reference them

This commit is contained in:
tianma-if committed 2026-09-23 18:45:23 +08:00
1 parent 672016cc3c
commit ad9fa6666a
2 files changed
+50 -3

No files matched your search

+10
View File
@@ -6,6 +6,7 @@ const {
createDesktopSyncDiagnosticText,
createDesktopSyncSummary,
isStagedResourceReferenced,
stagedIdsReferencedByUnsavedContent,
hasDesktopSyncStateReset,
mergeMemoIdMappings,
mergeSyncedMemos,
@@ -68,6 +69,15 @@ describe("desktop staged resource sync", () => {
], "stage-1")).toBe(true);
});
test("keeps staged bytes while an editor draft or visible image still uses the old URL", () => {
const rewrites = [{ memoId: "memo-1", placeholder: "edgeever-staged://stage-1", url: "/api/v1/resources/res-1/blob" }];
expect(stagedIdsReferencedByUnsavedContent(rewrites, [
{ contentJson: { type: "doc", content: [{ type: "image", attrs: { src: "edgeever-staged://stage-1" } }] } },
])).toEqual(new Set(["stage-1"]));
expect(stagedIdsReferencedByUnsavedContent(rewrites, ["edgeever-staged://stage-1"])).toEqual(new Set(["stage-1"]));
expect(stagedIdsReferencedByUnsavedContent(rewrites, ["edgeever-staged://stage-10"])).toEqual(new Set());
});
test("retains a temporary id mapping when a later sync phase fails", () => {
const retained = mergeMemoIdMappings(new Map(), new Map([["memo_local_1", "memo_remote_1"]]));
+40 -3
View File
@@ -17,6 +17,7 @@ import {
} from "@edgeever/shared";
import { api, ApiRequestError } from "@/lib/api";
import { isDesktopResourceRuntime, mapMarkdownResourceUrls, mapTiptapResourceUrls, toApiResourceUrl } from "@/lib/desktop-resources";
import { readEmergencyDraft } from "@/lib/emergency-draft";
import { notebookDeleteIdsFromPayload } from "@/lib/notebook-delete";
import { notifyMemoIdRemapped, notifyMemoSyncAcknowledged } from "@/lib/sync-events";
@@ -84,6 +85,41 @@ const stagedIdsReferencedByLocalMemos = async (rewrites: StagedResourceRewrite[]
return referenced;
};
export const stagedIdsReferencedByUnsavedContent = (
rewrites: StagedResourceRewrite[],
values: unknown[],
) => new Set(rewrites
.filter((rewrite) => isStagedResourceReferenced(values, rewrite.placeholder.slice("edgeever-staged://".length)))
.map((rewrite) => rewrite.placeholder.slice("edgeever-staged://".length)));
const stagedIdsReferencedByRenderer = async (rewrites: StagedResourceRewrite[]) => {
if (rewrites.length === 0) return new Set<string>();
const memoIds = [...new Set(rewrites.map((rewrite) => rewrite.memoId))];
try {
const { localDb } = await import("@/lib/local-db");
const [drafts, queued] = await Promise.all([
localDb.drafts.bulkGet(memoIds),
localDb.syncQueue.where("memoId").anyOf(memoIds).toArray(),
]);
const visibleSources = typeof document === "undefined" ? [] : [
...Array.from(document.querySelectorAll("[src], [href]"), (element) => (
element.getAttribute("src") ?? element.getAttribute("href")
)),
...Array.from(document.querySelectorAll("textarea"), (element) => element.value),
];
return stagedIdsReferencedByUnsavedContent(rewrites, [
...drafts,
...queued.map((item) => item.payload),
...memoIds.map((memoId) => readEmergencyDraft(memoId)),
...visibleSources,
]);
} catch {
// If drafts cannot be inspected, keep the bytes until a later sync can
// prove that no editor can save the old staged URL again.
return new Set(rewrites.map((rewrite) => rewrite.placeholder.slice("edgeever-staged://".length)));
}
};
const remapStagedResourceMemoIds = async (memoIdMappings: ReadonlyMap<string, string>) => {
if (memoIdMappings.size === 0) return;
// Keep sync compatible with a renderer hot-reload or an older native shell
@@ -748,9 +784,10 @@ export const syncDesktopData = () => {
const remainingReferenced = stillReferenced.size > 0
? await stagedIdsReferencedByLocalMemos(stagedResources.rewrites)
: stillReferenced;
await removeSyncedStagedResources(
stagedResources.stagedIds.filter((stagedId) => !remainingReferenced.has(stagedId)),
);
const rendererReferenced = await stagedIdsReferencedByRenderer(stagedResources.rewrites);
await removeSyncedStagedResources(stagedResources.stagedIds.filter((stagedId) => (
!remainingReferenced.has(stagedId) && !rendererReferenced.has(stagedId)
)));
}
phase = "read_status";
const remaining = await request("sync.status", {});