fix(updates): run pending plugin updates before restoring connections
This commit is contained in:
3 files changed
+134
-16
No files matched your search
+26
-15
@@ -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();
|
||||
|
||||
|
||||
+89
-1
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user