mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 20:52:01 +03:00
fix(desktop): skip differential updates when the cache is stale (#47925)
This commit is contained in:
committed by
GitHub
Unverified
parent
c3f1bdaf97
commit
22a534a0bb
@@ -9,7 +9,8 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
// Drives the updater the way the app does: start or check, then install like a button click. `calls` records the platform
|
||||
// operations in order; installs record the staged version they would apply.
|
||||
// operations in order; downloads record whether a differential download was allowed and installs record the staged
|
||||
// version they would apply.
|
||||
function setup(input?: {
|
||||
currentVersion?: string
|
||||
ready?: { version: string }
|
||||
@@ -29,10 +30,11 @@ function setup(input?: {
|
||||
},
|
||||
catch: (error) => error,
|
||||
}),
|
||||
stageUpdate: Effect.tryPromise(async () => {
|
||||
calls.push("download")
|
||||
await input?.stage?.()
|
||||
}),
|
||||
stageUpdate: (options) =>
|
||||
Effect.tryPromise(async () => {
|
||||
calls.push(options.differential ? "download" : "download:full")
|
||||
await input?.stage?.()
|
||||
}),
|
||||
installAndRestart: Effect.suspend(() => {
|
||||
calls.push(`install:${ready?.version}`)
|
||||
return Effect.tryPromise({
|
||||
@@ -76,6 +78,7 @@ describe("updater", () => {
|
||||
|
||||
await app.updater.start()
|
||||
|
||||
expect(app.calls).toEqual(["check", "download"])
|
||||
expect(await app.updater.getState()).toEqual({ status: "ready", version: "2.0.0" })
|
||||
expect(app.getReady()).toEqual({ version: "2.0.0" })
|
||||
})
|
||||
@@ -90,15 +93,37 @@ describe("updater", () => {
|
||||
expect(app.getReady()).toBeUndefined()
|
||||
})
|
||||
|
||||
test("revalidates a persisted target through the updater cache on launch", async () => {
|
||||
test("revalidates a persisted target through the updater cache on launch without a differential download", async () => {
|
||||
const app = setup({ ready: { version: "2.0.0" } })
|
||||
|
||||
await app.updater.start()
|
||||
|
||||
expect(app.calls).toEqual(["check", "download"])
|
||||
expect(app.calls).toEqual(["check", "download:full"])
|
||||
expect(await app.updater.getState()).toEqual({ status: "ready", version: "2.0.0" })
|
||||
})
|
||||
|
||||
test("keeps differential downloads after the persisted target was installed", async () => {
|
||||
const app = setup({ currentVersion: "2.0.0", ready: { version: "2.0.0" }, latest: () => "3.0.0" })
|
||||
|
||||
await app.updater.start()
|
||||
|
||||
expect(app.calls).toEqual(["check", "download"])
|
||||
expect(await app.updater.getState()).toEqual({ status: "ready", version: "3.0.0" })
|
||||
expect(app.getReady()).toEqual({ version: "3.0.0" })
|
||||
})
|
||||
|
||||
test("downloads newer releases in full once one is staged", async () => {
|
||||
let latest = "2.0.0"
|
||||
const app = setup({ latest: () => latest })
|
||||
await app.updater.start()
|
||||
|
||||
latest = "3.0.0"
|
||||
await app.updater.check()
|
||||
|
||||
expect(app.calls).toEqual(["check", "download", "check", "download:full"])
|
||||
expect(await app.updater.getState()).toEqual({ status: "ready", version: "3.0.0" })
|
||||
})
|
||||
|
||||
test("concurrent checks share one platform check", async () => {
|
||||
const app = setup()
|
||||
|
||||
@@ -140,7 +165,7 @@ describe("updater", () => {
|
||||
|
||||
expect(await app.updater.getState()).toEqual({ status: "installing", version: "2.0.0" })
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
expect(app.calls).toEqual(["check", "download", "check", "download", "prepare", "install:3.0.0"])
|
||||
expect(app.calls).toEqual(["check", "download", "check", "download:full", "prepare", "install:3.0.0"])
|
||||
expect(await app.updater.getState()).toEqual({ status: "installing", version: "3.0.0" })
|
||||
})
|
||||
|
||||
@@ -220,7 +245,7 @@ describe("updater", () => {
|
||||
await refresh
|
||||
expect(await app.updater.getState()).toEqual({ status: "installing", version: "3.0.0" })
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
expect(app.calls).toEqual(["check", "download", "check", "download", "prepare", "install:3.0.0"])
|
||||
expect(app.calls).toEqual(["check", "download", "check", "download:full", "prepare", "install:3.0.0"])
|
||||
})
|
||||
|
||||
test("returns to ready after a failed installation and allows a retry", async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { emitIpcEvent } from "../ipc-events"
|
||||
|
||||
export type Platform = {
|
||||
readonly checkForUpdate: Effect.Effect<string | undefined, unknown>
|
||||
readonly stageUpdate: Effect.Effect<unknown, unknown>
|
||||
readonly stageUpdate: (options: { readonly differential: boolean }) => Effect.Effect<unknown, unknown>
|
||||
readonly installAndRestart: Effect.Effect<never, unknown>
|
||||
readonly dispose: () => void
|
||||
}
|
||||
@@ -55,6 +55,22 @@ export const make = Effect.fn("Updater.make")(function* (dependencies: Dependenc
|
||||
listeners.forEach((listener) => listener(state))
|
||||
return state
|
||||
}
|
||||
// electron-updater builds NSIS deltas against the installer of the running version but reads the "old" blockmap from
|
||||
// the last download. Once a release is staged without installing, the two no longer match and every later delta fails
|
||||
// its checksum before falling back to a full download, so remember which release the cache holds and skip the attempt.
|
||||
let downloaded: string | undefined
|
||||
const stage = (platform: Platform, version: string) =>
|
||||
Effect.gen(function* () {
|
||||
if (downloaded)
|
||||
yield* Effect.logInfo("skipping differential download, updater cache is stale", {
|
||||
current: dependencies.currentVersion,
|
||||
staged: downloaded,
|
||||
version,
|
||||
})
|
||||
yield* platform.stageUpdate({ differential: !downloaded })
|
||||
downloaded = version
|
||||
yield* dependencies.persistence.set({ version })
|
||||
})
|
||||
const findAndStage = (platform: Platform) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.sync(() => transition({ status: "checking" }))
|
||||
@@ -64,8 +80,7 @@ export const make = Effect.fn("Updater.make")(function* (dependencies: Dependenc
|
||||
return transition({ status: "up-to-date" })
|
||||
}
|
||||
transition({ status: "downloading", version })
|
||||
yield* platform.stageUpdate
|
||||
yield* dependencies.persistence.set({ version })
|
||||
yield* stage(platform, version)
|
||||
return transition({ status: "ready", version })
|
||||
}).pipe(
|
||||
Effect.catch((error) =>
|
||||
@@ -78,8 +93,7 @@ export const make = Effect.fn("Updater.make")(function* (dependencies: Dependenc
|
||||
Effect.gen(function* () {
|
||||
const version = yield* platform.checkForUpdate
|
||||
if (!version || version === staged || version === dependencies.currentVersion) return state
|
||||
yield* platform.stageUpdate
|
||||
yield* dependencies.persistence.set({ version })
|
||||
yield* stage(platform, version)
|
||||
return transition({ status: installing ? "installing" : "ready", version })
|
||||
}).pipe(
|
||||
Effect.catch((error) =>
|
||||
@@ -137,6 +151,8 @@ export const make = Effect.fn("Updater.make")(function* (dependencies: Dependenc
|
||||
const start = Effect.gen(function* () {
|
||||
const ready = yield* dependencies.persistence.get
|
||||
if (ready?.version === dependencies.currentVersion) yield* dependencies.persistence.clear
|
||||
// Any other persisted target was downloaded by an earlier launch and never installed, so its blockmap is cached.
|
||||
if (ready && ready.version !== dependencies.currentVersion) downloaded = ready.version
|
||||
yield* check
|
||||
})
|
||||
const unsubscribe = (id: number) => {
|
||||
|
||||
@@ -37,16 +37,21 @@ export const make = Effect.gen(function* () {
|
||||
},
|
||||
catch: (error) => error,
|
||||
}),
|
||||
stageUpdate: stageUpdate(),
|
||||
stageUpdate,
|
||||
installAndRestart,
|
||||
dispose: () => autoUpdater.off("before-quit-for-update", beforeQuit),
|
||||
} satisfies Platform
|
||||
})
|
||||
|
||||
function stageUpdate() {
|
||||
function stageUpdate(options: { readonly differential: boolean }) {
|
||||
if (process.platform !== "darwin")
|
||||
return Effect.tryPromise({
|
||||
try: () => updateClient.downloadUpdate(),
|
||||
try: () => {
|
||||
// Only the NSIS cache goes stale: macOS refreshes its cached zip with every download and AppImage reads the
|
||||
// blockmap embedded in the running file.
|
||||
updateClient.disableDifferentialDownload = process.platform === "win32" && !options.differential
|
||||
return updateClient.downloadUpdate()
|
||||
},
|
||||
catch: (error) => error,
|
||||
}).pipe(Effect.asVoid)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user