fix(updates): run pending plugin updates before restoring connections

This commit is contained in:
zipg authored and GitHub committed 2026-09-22 11:58:02 +08:00
1 parent 639abb3993
commit d5ffa0f1d2
3 files changed
+134 -16

No files matched your search

+26 -15
View File
@@ -29,7 +29,16 @@ import { useMcpUpdateBadge } from "@/composables/useMcpUpdateBadge";
import { useComponentUpdates, type ComponentUpdateCategory } from "@/composables/useComponentUpdates";
import { COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import { driverStoreUpdateBadgeCount, showMcpUpdateBadge, showToolbarUpdateAction } from "@/lib/updates/updateBadges";
import { markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, shouldCloseUpdateCenterAfterComponentUpdate, takePendingComponentUpdatesAfterAppRestart, type PendingComponentUpdatePlan } from "@/lib/updates/componentUpdateOrchestration";
import {
hasPendingComponentUpdatesAfterAppRestart,
markPendingComponentUpdatesAfterAppUpdate,
resolveUpdateAllAction,
runPendingComponentUpdatesBeforePluginReconnect,
runPendingComponentUpdatePlan,
shouldCloseUpdateCenterAfterComponentUpdate,
takePendingComponentUpdatesAfterAppRestart,
type PendingComponentUpdatePlan,
} from "@/lib/updates/componentUpdateOrchestration";
import { isUpdatePreviewMockEnabled } from "@/lib/updates/updatePreviewMock";
import { useExportTracker } from "@/composables/useExportTracker";
import { useFileDrop } from "@/composables/useFileDrop";
@@ -3776,20 +3785,22 @@ async function initApp() {
onOptionalStateError: (error) => console.error("[STARTUP] settingsStore.initAiConfigs failed", error),
});
}
// Restored plugin tabs need the sidecar connection registry repopulated
// (see reconnectRestoredPluginTabs); kick it off before the heavier
// optional init so it races ahead of each plugin webview's first
// session/open. Fire-and-forget: it must never block startup.
void queryStore.reconnectRestoredPluginTabs();
await settingsStore.initDesktopSettings().catch(() => {});
if (isDesktop) {
updateWindowReady = true;
await initializeUpdatePreparation();
await initializeUpdater();
if (!isDetachedWindowContext) {
void consumePendingComponentUpdatesAfterRestart();
}
}
await runPendingComponentUpdatesBeforePluginReconnect({
hasPendingComponentUpdates: () => !isDetachedWindowContext && hasPendingComponentUpdatesAfterAppRestart(),
prepareStartup: async () => {
await settingsStore.initDesktopSettings().catch(() => {});
if (isDesktop) {
updateWindowReady = true;
await initializeUpdatePreparation();
await initializeUpdater();
}
},
consumePendingComponentUpdates: consumePendingComponentUpdatesAfterRestart,
// Restored plugin tabs need the sidecar connection registry repopulated
// (see reconnectRestoredPluginTabs). It is fire-and-forget so a slow
// sidecar or interactive prompt never blocks startup.
reconnectRestoredPluginTabs: async () => queryStore.reconnectRestoredPluginTabs(),
});
void promptTemplateStore.init();
@@ -1,7 +1,16 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from "vitest";
import { clearPendingComponentUpdatesAfterAppUpdate, markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, shouldCloseUpdateCenterAfterComponentUpdate, takePendingComponentUpdatesAfterAppRestart } from "@/lib/updates/componentUpdateOrchestration";
import {
clearPendingComponentUpdatesAfterAppUpdate,
hasPendingComponentUpdatesAfterAppRestart,
markPendingComponentUpdatesAfterAppUpdate,
resolveUpdateAllAction,
runPendingComponentUpdatesBeforePluginReconnect,
runPendingComponentUpdatePlan,
shouldCloseUpdateCenterAfterComponentUpdate,
takePendingComponentUpdatesAfterAppRestart,
} from "@/lib/updates/componentUpdateOrchestration";
describe("component update orchestration", () => {
beforeEach(() => {
@@ -16,6 +25,85 @@ describe("component update orchestration", () => {
expect(takePendingComponentUpdatesAfterAppRestart("0.6.17")).toEqual({ fromVersion: "0.6.16", targetVersion: "0.6.17", plan: { kind: "auto" } });
});
it("detects pending component updates before restored plugin tabs reconnect", () => {
expect(hasPendingComponentUpdatesAfterAppRestart()).toBe(false);
markPendingComponentUpdatesAfterAppUpdate("0.6.18", "0.6.19", { kind: "manual", categories: ["plugins"] });
expect(hasPendingComponentUpdatesAfterAppRestart()).toBe(true);
expect(takePendingComponentUpdatesAfterAppRestart("0.6.19")).not.toBeNull();
expect(hasPendingComponentUpdatesAfterAppRestart()).toBe(false);
});
it("consumes pending component updates before reconnecting restored plugin tabs", async () => {
const events: string[] = [];
await runPendingComponentUpdatesBeforePluginReconnect({
hasPendingComponentUpdates: () => true,
prepareStartup: async () => {
events.push("prepare");
},
consumePendingComponentUpdates: async () => {
events.push("consume");
},
reconnectRestoredPluginTabs: async () => {
events.push("reconnect");
},
});
expect(events).toEqual(["prepare", "consume", "reconnect"]);
});
it("reconnects restored plugin tabs immediately when no component update is pending", async () => {
const events: string[] = [];
let finishStartup = () => {};
const startupFinished = new Promise<void>((resolve) => {
finishStartup = resolve;
});
const startup = runPendingComponentUpdatesBeforePluginReconnect({
hasPendingComponentUpdates: () => false,
prepareStartup: async () => {
events.push("prepare");
await startupFinished;
},
consumePendingComponentUpdates: async () => {
events.push("consume");
},
reconnectRestoredPluginTabs: async () => {
events.push("reconnect");
},
});
await Promise.resolve();
expect(events).toEqual(["reconnect", "prepare"]);
finishStartup();
await startup;
expect(events).toEqual(["reconnect", "prepare"]);
});
it("still reconnects restored plugin tabs when pending component updates fail", async () => {
const events: string[] = [];
await expect(
runPendingComponentUpdatesBeforePluginReconnect({
hasPendingComponentUpdates: () => true,
prepareStartup: async () => {
events.push("prepare");
},
consumePendingComponentUpdates: async () => {
events.push("consume");
throw new Error("update failed");
},
reconnectRestoredPluginTabs: async () => {
events.push("reconnect");
},
}),
).rejects.toThrow("update failed");
expect(events).toEqual(["prepare", "consume", "reconnect"]);
});
it("consumes a successful restart exactly once", () => {
markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17");
@@ -79,6 +79,25 @@ export function clearPendingComponentUpdatesAfterAppUpdate() {
safeLocalStorageRemove(PENDING_COMPONENT_UPDATES_STORAGE_KEY);
}
export function hasPendingComponentUpdatesAfterAppRestart(): boolean {
return parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY)) !== null;
}
export async function runPendingComponentUpdatesBeforePluginReconnect(options: { hasPendingComponentUpdates: () => boolean; prepareStartup: () => Promise<void>; consumePendingComponentUpdates: () => Promise<void>; reconnectRestoredPluginTabs: () => Promise<void> }): Promise<void> {
if (!options.hasPendingComponentUpdates()) {
void options.reconnectRestoredPluginTabs();
await options.prepareStartup();
return;
}
try {
await options.prepareStartup();
await options.consumePendingComponentUpdates();
} finally {
void options.reconnectRestoredPluginTabs();
}
}
export function takePendingComponentUpdatesAfterAppRestart(currentVersion: string): PendingComponentUpdates | null {
const pending = parsePendingComponentUpdates(safeLocalStorageGet(PENDING_COMPONENT_UPDATES_STORAGE_KEY));
if (!pending) {