mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
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.
95 lines
2.7 KiB
TypeScript
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()
|
|
}
|
|
}
|
|
}
|
|
}
|