mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
* perf(renderer): stop full durable-state save on every top-level view switch (#9002) Persist activeView in a tiny profile-scoped sidecar instead of mutating the monolithic recovery snapshot. Active-view-only updates now bypass the broad UI normalization and durable save scheduler, while a 100ms atomic writer coalesces rapid switches and a synchronous shutdown checkpoint closes the immediate-exit race. Legacy state remains a migration and downgrade fallback. Coordinate renderer shutdown capture through one guarded checkpoint so workspace sessions and the active-view preference both survive graceful reloads, restarts, and quit cancellation. Add a persistence-boundary test proving the sidecar stays below 64 bytes while orca-data.json remains byte-for-byte unchanged, plus repeated Windows Electron restart coverage and a path-normalization-safe restart fixture. * harden active-view sidecar: prototype-safe validator, race-free async swap, independent shutdown flush - isTopLevelView uses Object.hasOwn so a corrupt sidecar can't smuggle inherited keys (constructor/__proto__) through as a valid view. - writeAsync guards the generation check and rename synchronously (renameSync) so a shutdown flushOrThrow can no longer interleave and let a stale async rename clobber the freshly-written view. - shutdown checkpoint flushes the durable store and the active-view sidecar in independent try/catch blocks so one store's failure can't skip the other. Added regression tests for all three. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { ActiveViewPreference, getActiveViewPreferenceFile } from './active-view-preference'
|
|
|
|
describe('ActiveViewPreference', () => {
|
|
let dir: string
|
|
let dataFile: string
|
|
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(join(tmpdir(), 'orca-active-view-'))
|
|
dataFile = join(dir, 'orca-data.json')
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('migrates the legacy durable-state value into the profile sidecar', async () => {
|
|
vi.useFakeTimers()
|
|
const preference = new ActiveViewPreference(dataFile, 'tasks')
|
|
|
|
expect(preference.get()).toBe('tasks')
|
|
expect(existsSync(getActiveViewPreferenceFile(dataFile))).toBe(false)
|
|
|
|
// The renderer reasserts its hydrated value once persistence is ready.
|
|
expect(preference.set('tasks')).toBe(false)
|
|
vi.advanceTimersByTime(100)
|
|
await preference.waitForPendingWrite()
|
|
|
|
expect(JSON.parse(readFileSync(getActiveViewPreferenceFile(dataFile), 'utf-8'))).toEqual({
|
|
activeView: 'tasks'
|
|
})
|
|
})
|
|
|
|
it('coalesces rapid switches into one tiny preference write', async () => {
|
|
vi.useFakeTimers()
|
|
const preference = new ActiveViewPreference(dataFile, 'terminal')
|
|
|
|
preference.set('settings')
|
|
vi.advanceTimersByTime(50)
|
|
preference.set('automations')
|
|
vi.advanceTimersByTime(50)
|
|
expect(existsSync(getActiveViewPreferenceFile(dataFile))).toBe(false)
|
|
|
|
vi.advanceTimersByTime(50)
|
|
await preference.waitForPendingWrite()
|
|
|
|
expect(JSON.parse(readFileSync(getActiveViewPreferenceFile(dataFile), 'utf-8'))).toEqual({
|
|
activeView: 'automations'
|
|
})
|
|
})
|
|
|
|
it('flushes synchronously for an immediate graceful exit', () => {
|
|
vi.useFakeTimers()
|
|
const preference = new ActiveViewPreference(dataFile, 'terminal')
|
|
|
|
preference.set('settings')
|
|
preference.flushOrThrow()
|
|
|
|
expect(JSON.parse(readFileSync(getActiveViewPreferenceFile(dataFile), 'utf-8'))).toEqual({
|
|
activeView: 'settings'
|
|
})
|
|
vi.advanceTimersByTime(100)
|
|
expect(JSON.parse(readFileSync(getActiveViewPreferenceFile(dataFile), 'utf-8'))).toEqual({
|
|
activeView: 'settings'
|
|
})
|
|
})
|
|
|
|
it('ignores an invalid sidecar and invalid updates', () => {
|
|
writeFileSync(getActiveViewPreferenceFile(dataFile), '{"activeView":"unknown"}', 'utf-8')
|
|
const preference = new ActiveViewPreference(dataFile, 'not-a-view')
|
|
|
|
expect(preference.get()).toBe('terminal')
|
|
expect(preference.set('also-not-a-view')).toBe(false)
|
|
expect(preference.get()).toBe('terminal')
|
|
})
|
|
|
|
it('rejects inherited object keys from a corrupt sidecar', () => {
|
|
// Why: `constructor`/`__proto__` are truthy under `in`; the sidecar must not
|
|
// treat them as a valid view and leave the main surface blank.
|
|
writeFileSync(getActiveViewPreferenceFile(dataFile), '{"activeView":"constructor"}', 'utf-8')
|
|
const preference = new ActiveViewPreference(dataFile, 'tasks')
|
|
|
|
expect(preference.get()).toBe('tasks')
|
|
expect(preference.set('__proto__')).toBe(false)
|
|
expect(preference.get()).toBe('tasks')
|
|
})
|
|
})
|