Files
orca/src/shared/renderer-restart-preparation.ts
T
Brennan Benson 194e1a8d4d fix(persistence): make the renderer unload checkpoint durably flush before reporting success (#12387)
The sync before-unload checkpoint staged renderer state and then queued
store.flushPendingAsync() fire-and-forget, so reload/restart/update paths
navigated while the staged session, scrollback and UI state were still
only in memory. Quit is covered by the will-quit flush barrier; those
paths were not.

Keep staging synchronous (no sync durable writes), but record the flush
outcome and expose it on app:await-before-unload-checkpoint. Restart,
updater install and lazy-chunk recovery reload now join that write before
navigating and abort the attempt when it fails or outlives a 20s deadline.
2026-08-03 19:18:40 -07:00

95 lines
2.7 KiB
TypeScript

import {
ORCA_EDITOR_PREPARE_HOT_EXIT_EVENT,
type EditorPrepareHotExitDetail
} from './editor-save-events'
import type { UpdateStatus } from './types'
export type AppRestartPrepOptions = {
startedEventName: string
abortedEventName: string
/** Joins the durable write of the state the checkpoint staged; rejects if it failed. */
awaitCheckpoint: () => Promise<void>
}
function requestEditorHotExitBackup(eventTarget: EventTarget): Promise<void> {
return new Promise<void>((resolve, reject) => {
let claimed = false
eventTarget.dispatchEvent(
new CustomEvent<EditorPrepareHotExitDetail>(ORCA_EDITOR_PREPARE_HOT_EXIT_EVENT, {
detail: {
claim: () => {
claimed = true
},
resolve,
reject: (message) => {
reject(new Error(message))
}
}
})
)
// Why: restart paths can run before the editor autosave controller mounts.
// With no claimant, there are no renderer-owned dirty buffers to back up.
if (!claimed) {
resolve()
}
})
}
export async function prepareRendererForAppRestart(
eventTarget: EventTarget,
{ startedEventName, abortedEventName, awaitCheckpoint }: AppRestartPrepOptions
): Promise<void> {
eventTarget.dispatchEvent(new Event(startedEventName))
try {
await requestEditorHotExitBackup(eventTarget)
// Why: update installs can bypass native close. A cancelable synthetic
// unload both captures mounted terminals and reports checkpoint failure.
const accepted = eventTarget.dispatchEvent(new Event('beforeunload', { cancelable: true }))
if (!accepted) {
throw new Error('Renderer shutdown checkpoint was not completed.')
}
// Why: the checkpoint only stages synchronously. Navigating before that
// write lands loses the session snapshot to a crash or power loss.
await awaitCheckpoint()
} catch (error) {
eventTarget.dispatchEvent(new Event(abortedEventName))
throw error
}
}
export type UpdaterQuitAbortRelay = {
markPrepared: () => void
abort: () => void
handleStatus: (status: UpdateStatus) => void
}
export function createUpdaterQuitAbortRelay(
eventTarget: EventTarget,
abortedEventName: string
): UpdaterQuitAbortRelay {
let prepared = false
const abort = (): void => {
if (!prepared) {
return
}
prepared = false
eventTarget.dispatchEvent(new Event(abortedEventName))
}
return {
markPrepared(): void {
prepared = true
},
abort,
handleStatus(status): void {
// Why: quitAndInstall IPC resolves after scheduling; a later updater
// error is the authoritative signal that the app will remain open.
if (status.state === 'error') {
abort()
}
}
}
}