fix(sync): guard stale installed refreshes

This commit is contained in:
2026-05-19 02:16:39 +08:00
parent 34551fce7b
commit 839f4017f8
3 changed files with 109 additions and 11 deletions
+18 -9
View File
@@ -1541,7 +1541,9 @@ const loadDownloadedHistory = async (): Promise<void> => {
}
};
const refreshInstalledSyncCandidates = async (): Promise<void> => {
const refreshInstalledSyncCandidates = async (
isCurrentRequest: () => boolean,
): Promise<boolean> => {
const origins: Array<"spark" | "apm"> = [];
if (isOriginEnabled(storeFilter.value, "spark") && sparkAvailable.value) {
origins.push("spark");
@@ -1572,11 +1574,16 @@ const refreshInstalledSyncCandidates = async (): Promise<void> => {
}),
);
if (!isCurrentRequest()) {
return false;
}
syncCandidateApps.value = mergeInstalledApps(
syncCandidateApps.value,
refreshedApps,
origins,
);
return true;
};
const syncInstalledAppsToAccount = async (): Promise<void> => {
@@ -1588,13 +1595,12 @@ const syncInstalledAppsToAccount = async (): Promise<void> => {
syncRequestGeneration.value = generation;
syncLoading.value = true;
try {
await refreshInstalledSyncCandidates();
if (
syncRequestGeneration.value !== generation ||
currentUser.value?.id !== userId
) {
return;
}
const refreshed = await refreshInstalledSyncCandidates(
() =>
syncRequestGeneration.value === generation &&
currentUser.value?.id === userId,
);
if (!refreshed) return;
const items = buildSyncItems(syncCandidateApps.value);
await uploadSyncedAppList({
clientArch: window.apm_store.arch || "amd64",
@@ -1641,7 +1647,10 @@ const openRestoreFromAccount = async (): Promise<void> => {
restoreError.value = "";
restoreItems.value = [];
try {
await refreshInstalledSyncCandidates();
const refreshed = await refreshInstalledSyncCandidates(() =>
isCurrentRestoreRequest(generation, userId),
);
if (!refreshed) return;
const result = await fetchSyncedAppList();
if (!isCurrentRestoreRequest(generation, userId)) return;
restoreItems.value = result?.items || [];
@@ -583,4 +583,83 @@ describe("App account placeholders", () => {
});
syncUpload.resolve(syncedList([]));
});
it("does not upload stale sync candidates after logout", async () => {
const slowInstalled = createDeferred<{
success: true;
apps: Array<{
pkgname: string;
name: string;
version: string;
arch: string;
flags: string;
origin: "apm";
}>;
}>();
let listInstalledCalls = 0;
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "apm";
if (channel === "check-spark-available") return false;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "get-system-info") return { distro: "deepin 25" };
if (channel === "list-installed") {
listInstalledCalls += 1;
if (listInstalledCalls === 1) return slowInstalled.promise;
return { success: true, apps: [] };
}
return [];
});
vi.mocked(uploadSyncedAppList).mockResolvedValue(syncedList([]));
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
await fireEvent.click(
await screen.findByRole("button", { name: "立即同步" }),
);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByText("退出登录")) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByText("退出登录"));
slowInstalled.resolve({
success: true,
apps: [
{
pkgname: "wps",
name: "WPS",
version: "1.0.0",
arch: "amd64",
flags: "installed",
origin: "apm",
},
],
});
await slowInstalled.promise;
await Promise.resolve();
setSecondUserSession();
const secondUserButton = await screen.findByRole("button", {
name: /^Second User$/,
});
if (!screen.queryByText("用户管理")) {
await fireEvent.click(secondUserButton);
}
await fireEvent.click(await screen.findByText("用户管理"));
await fireEvent.click(
await screen.findByRole("button", { name: "立即同步" }),
);
await waitFor(() => {
expect(uploadSyncedAppList).toHaveBeenCalledWith(
expect.objectContaining({ items: [] }),
);
});
});
});
+12 -2
View File
@@ -254,11 +254,21 @@ const emit = defineEmits<{
}>();
const handleSyncClick = () => {
emit(props.loggedIn ? "sync-to-account" : "request-login");
if (props.loggedIn) {
emit("sync-to-account");
return;
}
emit("request-login");
};
const handleRestoreClick = () => {
emit(props.loggedIn ? "restore-from-account" : "request-login");
if (props.loggedIn) {
emit("restore-from-account");
return;
}
emit("request-login");
};
const onOverlayWheel = (e: WheelEvent) => {