fix(settings): preserve RAM retries and find matching Java on PATH

This commit is contained in:
Codex
2026-09-09 18:57:21 +03:00
parent 3f3be65a3f
commit cd5c675637
4 changed files with 130 additions and 14 deletions
+12 -7
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { createSerialQueue, errorMessage } from '../services/async'
import { createSaveIntent, createSerialQueue, errorMessage } from '../services/async'
import { isNative, native } from '../services/native'
import { defaultSettings } from '../state/settings'
import type { LauncherSettings } from '../types/launcher'
@@ -12,7 +12,7 @@ export function useSettings() {
const [loadAttempt, setLoadAttempt] = useState(0)
const current = useRef(settings)
const durable = useRef(settings)
const revision = useRef(0)
const intent = useRef(createSaveIntent<LauncherSettings>())
const queue = useRef(createSerialQueue())
useEffect(() => {
@@ -38,21 +38,23 @@ export function useSettings() {
setSettings(next)
setError(null)
if (!isNative()) return
const requestRevision = ++revision.current
const request = intent.current.begin(next)
setSaving(true)
void queue.current.enqueue(() => native.saveSettings(next)).then((value) => {
durable.current = value
if (revision.current === requestRevision) {
intent.current.succeeded(request)
if (intent.current.isLatest(request)) {
current.current = value
setSettings(value)
}
}).catch((reason: unknown) => {
if (revision.current !== requestRevision) return
if (!intent.current.isLatest(request)) return
intent.current.failed(request)
current.current = durable.current
setSettings(durable.current)
setError(errorMessage(reason, 'Не удалось сохранить настройки'))
}).finally(() => {
if (revision.current === requestRevision) setSaving(false)
if (intent.current.isLatest(request)) setSaving(false)
})
}
@@ -61,7 +63,10 @@ export function useSettings() {
updateRam: (memoryGb: number) => save({ memoryMb: memoryGb * 1024 }),
retry: () => {
if (!loaded) setLoadAttempt((attempt) => attempt + 1)
else save({})
else {
const failed = intent.current.retryValue()
if (failed) save(failed)
}
},
}
}
+48 -1
View File
@@ -1,6 +1,6 @@
import { deepStrictEqual, equal, rejects } from 'node:assert/strict'
import { test } from 'node:test'
import { createRequestScope, createSerialQueue, createSubscription, errorMessage, singleFlight } from './async.ts'
import { createSaveIntent, createRequestScope, createSerialQueue, createSubscription, errorMessage, singleFlight } from './async.ts'
function deferred<T>() {
let resolve!: (value: T) => void
@@ -111,3 +111,50 @@ test('logout or a newer challenge invalidates a delayed account/link response',
equal(belongsToNewChallenge(), true)
equal(belongsToAccount(), false)
})
test('RAM retry retains the failed 8 GB choice after rollback to durable 6 GB', async () => {
const intent = createSaveIntent<{ memoryMb: number }>()
const queue = createSerialQueue()
let current = { memoryMb: 6 * 1024 }
let durable = current
const writes: number[] = []
const save = async (next: typeof current, fail: boolean) => {
current = next
const request = intent.begin(next)
try {
const value = await queue.enqueue(async () => {
writes.push(next.memoryMb)
if (fail) throw new Error('disk full')
return next
})
durable = value
intent.succeeded(request)
} catch {
intent.failed(request)
current = durable
}
}
await save({ memoryMb: 8 * 1024 }, true)
equal(current.memoryMb, 6 * 1024)
await save(intent.retryValue()!, false)
deepStrictEqual(writes, [8 * 1024, 8 * 1024])
equal(durable.memoryMb, 8 * 1024)
equal(intent.retryValue(), null)
})
test('older settings failure cannot replace a newer choice or remain retryable after success', () => {
const intent = createSaveIntent<number>()
const older = intent.begin(8)
const newer = intent.begin(10)
intent.failed(older)
equal(intent.retryValue(), null)
intent.failed(newer)
equal(intent.retryValue(), 10)
const latest = intent.begin(12)
intent.succeeded(older)
intent.failed(latest)
equal(intent.retryValue(), 12)
intent.succeeded(latest)
equal(intent.retryValue(), null)
})
+13
View File
@@ -44,6 +44,19 @@ export function createSerialQueue() {
}
}
/** Retain the latest failed user choice even when UI rolls back to disk state. */
export function createSaveIntent<T>() {
let revision = 0
let failed: T | null = null
return {
begin: (value: T) => { failed = null; return { revision: ++revision, value } },
isLatest: (request: { revision: number }) => request.revision === revision,
succeeded: (request: { revision: number }) => { if (request.revision === revision) failed = null },
failed: (request: { revision: number; value: T }) => { if (request.revision === revision) failed = request.value },
retryValue: () => failed,
}
}
/** Handles unmount before asynchronous native listener registration finishes. */
export function createSubscription(
registrations: readonly Promise<() => void>[],