1
This commit is contained in:
@@ -4,37 +4,71 @@ import {
|
||||
clampVolume,
|
||||
DEFAULT_MUSIC_VOLUME,
|
||||
} from '../persistence/gameSettingsStorage';
|
||||
import { isAbortError } from '../services/apiClient';
|
||||
import { getSettings, putSettings } from '../services/storageService';
|
||||
|
||||
const SETTINGS_SYNC_DELAY_MS = 180;
|
||||
|
||||
export function useGameSettings() {
|
||||
const [musicVolume, setMusicVolumeState] = useState(DEFAULT_MUSIC_VOLUME);
|
||||
const [hasHydratedSettings, setHasHydratedSettings] = useState(false);
|
||||
const [isHydratingSettings, setIsHydratingSettings] = useState(true);
|
||||
const [isPersistingSettings, setIsPersistingSettings] = useState(false);
|
||||
const [settingsError, setSettingsError] = useState<string | null>(null);
|
||||
const lastSyncedVolumeRef = useRef(DEFAULT_MUSIC_VOLUME);
|
||||
const hydrateControllerRef = useRef<AbortController | null>(null);
|
||||
const persistControllerRef = useRef<AbortController | null>(null);
|
||||
const persistRequestIdRef = useRef(0);
|
||||
|
||||
const abortActivePersist = useCallback(() => {
|
||||
persistControllerRef.current?.abort();
|
||||
persistControllerRef.current = null;
|
||||
setIsPersistingSettings(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
const controller = new AbortController();
|
||||
hydrateControllerRef.current = controller;
|
||||
setIsHydratingSettings(true);
|
||||
|
||||
void getSettings()
|
||||
void getSettings({ signal: controller.signal })
|
||||
.then((settings) => {
|
||||
if (!isActive) return;
|
||||
const nextVolume = clampVolume(settings.musicVolume);
|
||||
lastSyncedVolumeRef.current = nextVolume;
|
||||
setMusicVolumeState(nextVolume);
|
||||
setSettingsError(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isAbortError(error)) {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : '读取远端设置失败';
|
||||
setSettingsError(message);
|
||||
console.warn('[useGameSettings] failed to load remote settings', error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (isActive) {
|
||||
if (hydrateControllerRef.current === controller) {
|
||||
hydrateControllerRef.current = null;
|
||||
setIsHydratingSettings(false);
|
||||
setHasHydratedSettings(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
controller.abort();
|
||||
if (hydrateControllerRef.current === controller) {
|
||||
hydrateControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
hydrateControllerRef.current?.abort();
|
||||
persistControllerRef.current?.abort();
|
||||
persistControllerRef.current = null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasHydratedSettings) {
|
||||
return;
|
||||
@@ -44,21 +78,49 @@ export function useGameSettings() {
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
abortActivePersist();
|
||||
|
||||
void putSettings({musicVolume})
|
||||
.then((settings) => {
|
||||
if (!isActive) return;
|
||||
lastSyncedVolumeRef.current = clampVolume(settings.musicVolume);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[useGameSettings] failed to persist remote settings', error);
|
||||
});
|
||||
const requestId = persistRequestIdRef.current + 1;
|
||||
persistRequestIdRef.current = requestId;
|
||||
const controller = new AbortController();
|
||||
persistControllerRef.current = controller;
|
||||
setIsPersistingSettings(true);
|
||||
setSettingsError(null);
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [hasHydratedSettings, musicVolume]);
|
||||
void putSettings({ musicVolume }, { signal: controller.signal })
|
||||
.then((settings) => {
|
||||
if (persistRequestIdRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextVolume = clampVolume(settings.musicVolume);
|
||||
lastSyncedVolumeRef.current = nextVolume;
|
||||
setMusicVolumeState((currentValue) =>
|
||||
currentValue === nextVolume ? currentValue : nextVolume,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isAbortError(error)) {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : '保存远端设置失败';
|
||||
if (persistRequestIdRef.current === requestId) {
|
||||
setSettingsError(message);
|
||||
}
|
||||
console.warn('[useGameSettings] failed to persist remote settings', error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (persistControllerRef.current === controller) {
|
||||
persistControllerRef.current = null;
|
||||
setIsPersistingSettings(false);
|
||||
}
|
||||
});
|
||||
}, SETTINGS_SYNC_DELAY_MS);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [abortActivePersist, hasHydratedSettings, musicVolume]);
|
||||
|
||||
const setMusicVolume = useCallback((value: number) => {
|
||||
setMusicVolumeState(clampVolume(value));
|
||||
@@ -67,5 +129,9 @@ export function useGameSettings() {
|
||||
return {
|
||||
musicVolume,
|
||||
setMusicVolume,
|
||||
hasHydratedSettings,
|
||||
isHydratingSettings,
|
||||
isPersistingSettings,
|
||||
settingsError,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user