summaryrefslogtreecommitdiff
path: root/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'src/hooks')
-rw-r--r--src/hooks/useGameConfiguration.ts158
-rw-r--r--src/hooks/usePerAppWorkarounds.ts234
2 files changed, 302 insertions, 90 deletions
diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts
index 6d1fe6a..c66596a 100644
--- a/src/hooks/useGameConfiguration.ts
+++ b/src/hooks/useGameConfiguration.ts
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuickAccessVisible } from "@decky/api";
import { Router } from "@decky/ui";
-import { getGameConfigs, getInstalledGames, updateGameConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type GlobalConfig, type InstalledGame } from "../api/lsfgApi";
+import { getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi";
import { ConfigurationData, getDefaults } from "../config/configSchema";
-import { applyWorkaroundState, cleanupLegacySteamLaunchOptions, cleanupSteamLaunchOptions, getDefaultWorkaroundState, updateSteamLaunchOptions } from "../utils/steamLaunchOptions";
+import { cleanupLegacySteamLaunchOptions, getDefaultWrapperPath, hasWrapperLaunchIntegration, installWrapperIntegration, isLegacyWrapperToken, readSteamLaunchOptions, removeWrapperIntegration } from "../utils/steamLaunchOptions";
import { showErrorToast } from "../utils/toastUtils";
export interface GameTarget extends InstalledGame { configured: boolean; }
@@ -32,6 +32,19 @@ function mergeInstalledGames(backendGames: InstalledGame[], shortcutGames: Insta
return Array.from(games.values());
}
+const DEFAULT_WORKAROUND_STATE: WorkaroundState = {
+ dxvkFrameRate: 0,
+ disableGamescopeWsi: true,
+ disableHdr: true,
+ disableSteamdeckMode: false,
+ disableVkbasalt: false,
+ enableZink: false,
+};
+
+function asError(error: unknown): Error {
+ return error instanceof Error ? error : new Error(String(error));
+}
+
export function useGameConfiguration() {
const [games, setGames] = useState<GameConfigEntry[]>([]);
const [globalConfig, setGlobalConfig] = useState<GlobalConfig>({ dll: "", no_fp16: false });
@@ -95,39 +108,107 @@ export function useGameConfiguration() {
const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]);
const config = games.find((game) => game.appid === selectedAppId)?.config || template;
- const cleanupTargetLaunchOptions = useCallback(async (target: GameTarget): Promise<boolean> => {
+ const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise<boolean> => {
if (!installedGames.some((game) => game.appid === target.appid)) return true;
+ const appId = Number(target.appid);
try {
- await cleanupLegacySteamLaunchOptions(Number(target.appid), target.nonSteam);
- return true;
- } catch (error) {
- showErrorToast("Could not update Steam launch options", error instanceof Error ? error.message : String(error));
- return false;
- }
- }, [installedGames]);
+ const existing = await getWorkaroundState(target.appid);
+ if (!existing.success) throw new Error(existing.error || "Could not read workaround state");
+ const current = await readSteamLaunchOptions(appId, target.nonSteam);
+ const wrapperPath = existing.wrapper_path || getDefaultWrapperPath();
+ const oldState = existing.state;
+ const oldShortcutExe = existing.shortcut_exe || undefined;
+ const oldCommandTokenAdded = existing.command_token_added === true;
+ if (target.nonSteam && oldState && current.target === wrapperPath && !oldShortcutExe) {
+ throw new Error("Managed shortcut Target has no saved original executable");
+ }
+ if (target.nonSteam && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) {
+ throw new Error("Shortcut Target changed externally; refusing to replace it");
+ }
+ if (target.nonSteam && !oldState && current.target === wrapperPath) {
+ throw new Error("Shortcut Target is already the managed wrapper but its original Target is unknown");
+ }
+ const state = oldState || { ...DEFAULT_WORKAROUND_STATE };
+ const originalExecutable = target.nonSteam ? (oldShortcutExe || current.target) : undefined;
+ const initialIntegration = target.nonSteam
+ ? current.target === wrapperPath
+ : hasWrapperLaunchIntegration(current.options, wrapperPath);
+ const initialStateResult = await setWorkaroundState(
+ target.appid,
+ state,
+ originalExecutable || null,
+ oldCommandTokenAdded,
+ );
+ if (!initialStateResult.success) throw new Error(initialStateResult.error || "Could not create workaround state");
- const removeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise<boolean> => {
- if (!installedGames.some((game) => game.appid === target.appid)) return true;
- try {
- await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam);
- return true;
+ let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null;
+ try {
+ integration = await installWrapperIntegration(appId, target.nonSteam, wrapperPath, oldCommandTokenAdded);
+ const finalStateResult = await setWorkaroundState(
+ target.appid,
+ state,
+ target.nonSteam ? (integration.originalExecutable || originalExecutable || null) : null,
+ integration.commandTokenAdded,
+ );
+ if (!finalStateResult.success) throw new Error(finalStateResult.error || "Could not finalize workaround state");
+ return true;
+ } catch (error) {
+ let rollbackSucceeded = true;
+ if (!initialIntegration && integration) {
+ try {
+ await removeWrapperIntegration(
+ appId,
+ target.nonSteam,
+ wrapperPath,
+ target.nonSteam ? (integration?.originalExecutable || originalExecutable) : undefined,
+ integration?.commandTokenAdded ?? oldCommandTokenAdded,
+ );
+ } catch (rollbackError) {
+ showErrorToast("Workaround rollback failed", asError(rollbackError).message);
+ rollbackSucceeded = false;
+ }
+ }
+ if (rollbackSucceeded) {
+ const restored = oldState
+ ? await setWorkaroundState(target.appid, oldState, oldShortcutExe || null, oldCommandTokenAdded)
+ : await removeWorkaroundState(target.appid);
+ if (!restored.success) throw new Error(restored.error || "Could not roll back workaround state");
+ }
+ throw error;
+ }
} catch (error) {
- showErrorToast("Could not clean up Steam launch options", error instanceof Error ? error.message : String(error));
+ showErrorToast("Could not initialize workarounds", asError(error).message);
return false;
}
}, [installedGames]);
- const initializeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise<boolean> => {
+ const removeTargetWorkarounds = useCallback(async (target: GameTarget): Promise<boolean> => {
if (!installedGames.some((game) => game.appid === target.appid)) return true;
+ const appId = Number(target.appid);
try {
- await updateSteamLaunchOptions(
- Number(target.appid),
- target.nonSteam,
- (options) => applyWorkaroundState(options, getDefaultWorkaroundState()),
- );
+ const existing = await getWorkaroundState(target.appid);
+ if (!existing.success) throw new Error(existing.error || "Could not read workaround state");
+ const wrapperPath = existing.wrapper_path || getDefaultWrapperPath();
+ if (existing.state) {
+ await removeWrapperIntegration(
+ appId,
+ target.nonSteam,
+ wrapperPath,
+ existing.shortcut_exe || undefined,
+ existing.command_token_added === true,
+ );
+ } else {
+ const current = await readSteamLaunchOptions(appId, target.nonSteam);
+ if (target.nonSteam && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) {
+ throw new Error("Shortcut Target is a frame-generation wrapper but its original Target is unknown");
+ }
+ await cleanupLegacySteamLaunchOptions(appId, target.nonSteam, wrapperPath);
+ }
+ const removed = await removeWorkaroundState(target.appid);
+ if (!removed.success) throw new Error(removed.error || "Could not remove workaround state");
return true;
} catch (error) {
- showErrorToast("Could not initialize Steam launch options", error instanceof Error ? error.message : String(error));
+ showErrorToast("Could not clean up game workarounds", asError(error).message);
return false;
}
}, [installedGames]);
@@ -135,37 +216,46 @@ export function useGameConfiguration() {
const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => {
const selectedTarget = targets.find((target) => target.appid === selectedAppId);
if (!selectedTarget?.name) return;
- if (cleanupLaunchOptions && !(await cleanupTargetLaunchOptions(selectedTarget))) return;
+ // The profile owns its wrapper integration. Keep this check on every
+ // configuration save so an external edit is detected before the profile
+ // is changed; toggles update the sidecar only.
+ if (cleanupLaunchOptions && !(await ensureTargetWorkarounds(selectedTarget))) return;
const result = await updateGameConfig(selectedAppId, selectedTarget.name, next);
if (result.success) await load();
- }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]);
+ }, [ensureTargetWorkarounds, load, selectedAppId, targets]);
const enable = useCallback(async (appid: string) => {
const target = targets.find((item) => item.appid === appid);
if (!target?.name) return false;
- if (!(await initializeTargetLaunchOptions(target))) return false;
+ if (!(await ensureTargetWorkarounds(target))) return false;
const result = await updateGameConfig(appid, target.name, template);
if (result.success) await load();
+ else await removeTargetWorkarounds(target);
return result.success;
- }, [initializeTargetLaunchOptions, load, targets, template]);
+ }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]);
const enableAll = useCallback(async (): Promise<void> => {
const available = targets.filter((target) => !target.configured && target.name);
if (available.length === 0) return;
for (const target of available) {
- if (!(await initializeTargetLaunchOptions(target))) return;
+ if (!(await ensureTargetWorkarounds(target))) return;
const result = await updateGameConfig(target.appid, target.name, template);
if (!result.success) {
showErrorToast("Could not enable all games", result.error || "A game profile could not be created");
+ await removeTargetWorkarounds(target);
return;
}
}
await load();
- }, [initializeTargetLaunchOptions, load, targets, template]);
+ }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]);
+ const repair = useCallback(async (appid: string): Promise<boolean> => {
+ const target = targets.find((item) => item.appid === appid);
+ return target ? ensureTargetWorkarounds(target) : false;
+ }, [ensureTargetWorkarounds, targets]);
const resetSelected = useCallback(async () => {
if (selectedAppId) {
const selectedTarget = targets.find((target) => target.appid === selectedAppId);
- if (selectedTarget && !(await removeTargetLaunchOptions(selectedTarget))) return;
+ if (selectedTarget && !(await removeTargetWorkarounds(selectedTarget))) return;
const result = await resetGameConfig(selectedAppId);
if (result.success) {
setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current);
@@ -173,10 +263,10 @@ export function useGameConfiguration() {
await load();
}
}
- }, [load, removeTargetLaunchOptions, selectedAppId, targets]);
+ }, [load, removeTargetWorkarounds, selectedAppId, targets]);
const resetAll = useCallback(async () => {
for (const target of targets.filter((item) => item.configured)) {
- if (!(await removeTargetLaunchOptions(target))) return;
+ if (!(await removeTargetWorkarounds(target))) return;
}
const result = await resetAllGameConfigs();
if (result.success) {
@@ -184,7 +274,7 @@ export function useGameConfiguration() {
setSelectedAppId("");
await load();
}
- }, [load, removeTargetLaunchOptions, targets]);
+ }, [load, removeTargetWorkarounds, targets]);
- return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, resetSelected, resetAll, reload: load };
+ return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load };
}
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
index a937780..9e283db 100644
--- a/src/hooks/usePerAppWorkarounds.ts
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -1,29 +1,50 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
- applyWorkaroundChange,
- parseWorkaroundOptions,
+ getWorkaroundState,
+ removeWorkaroundState,
+ setWorkaroundState,
+ type WorkaroundState,
+} from "../api/lsfgApi";
+import {
+ getDefaultWrapperPath,
+ hasWrapperLaunchIntegration,
+ installWrapperIntegration,
+ isLegacyWrapperToken,
readSteamLaunchOptions,
+ removeWrapperIntegration,
subscribeSteamLaunchOptions,
- updateSteamLaunchOptions,
- type ParsedWorkaroundOptions,
type SteamLaunchOptionsSnapshot,
- type WorkaroundField,
} from "../utils/steamLaunchOptions";
import { showErrorToast } from "../utils/toastUtils";
+export type WorkaroundField = keyof WorkaroundState;
export type WorkaroundLoadStatus = "loading" | "ready" | "error";
const SLIDER_DEBOUNCE_MS = 250;
+const DEFAULT_WORKAROUND_STATE: WorkaroundState = {
+ dxvkFrameRate: 0,
+ disableGamescopeWsi: true,
+ disableHdr: true,
+ disableSteamdeckMode: false,
+ disableVkbasalt: false,
+ enableZink: false,
+};
+
interface PendingSliderUpdate {
timer: number;
value: number;
waiters: Array<(success: boolean) => void>;
}
-interface WorkaroundSnapshot {
+export interface WorkaroundSnapshot {
steam: SteamLaunchOptionsSnapshot;
- parsed: ParsedWorkaroundOptions;
+ state: WorkaroundState;
+ wrapperPath: string;
+ wrapperOwned: boolean;
+ integrationInstalled: boolean;
+ commandTokenAdded: boolean;
+ shortcutExe?: string | null;
}
interface PerAppWorkarounds {
@@ -38,8 +59,84 @@ function asError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
-function makeSnapshot(steam: SteamLaunchOptionsSnapshot): WorkaroundSnapshot {
- return { steam, parsed: parseWorkaroundOptions(steam.options) };
+function integrationIsInstalled(
+ steam: SteamLaunchOptionsSnapshot,
+ nonSteam: boolean,
+ wrapperPath: string,
+): boolean {
+ return nonSteam ? steam.target === wrapperPath : hasWrapperLaunchIntegration(steam.options, wrapperPath);
+}
+
+function makeSnapshot(
+ steam: SteamLaunchOptionsSnapshot,
+ result: Awaited<ReturnType<typeof getWorkaroundState>>,
+ nonSteam: boolean,
+): WorkaroundSnapshot {
+ if (!result.state) throw new Error("Workaround state is not initialized for this profile");
+ const wrapperPath = result.wrapper_path || getDefaultWrapperPath();
+ if (nonSteam && steam.target === wrapperPath && !result.shortcut_exe) {
+ throw new Error("Managed shortcut Target has no saved original executable");
+ }
+ return {
+ steam,
+ state: result.state,
+ wrapperPath,
+ wrapperOwned: result.wrapper_owned === true,
+ integrationInstalled: integrationIsInstalled(steam, nonSteam, wrapperPath),
+ commandTokenAdded: result.command_token_added === true,
+ shortcutExe: result.shortcut_exe,
+ };
+}
+
+async function adoptWorkaroundState(
+ appId: string,
+ nonSteam: boolean,
+ steam: SteamLaunchOptionsSnapshot,
+ wrapperPath: string,
+): Promise<WorkaroundSnapshot> {
+ if (nonSteam && (!steam.target || steam.target === wrapperPath || isLegacyWrapperToken(steam.target))) {
+ throw new Error("Shortcut Target is a wrapper but its original Target is unknown");
+ }
+ const originalExecutable = nonSteam ? steam.target : null;
+ const initial = await setWorkaroundState(appId, DEFAULT_WORKAROUND_STATE, originalExecutable, false);
+ if (!initial.success) throw new Error(initial.error || "Could not create workaround state");
+ let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null;
+ try {
+ integration = await installWrapperIntegration(
+ Number(appId),
+ nonSteam,
+ wrapperPath,
+ );
+ const finalized = await setWorkaroundState(
+ appId,
+ DEFAULT_WORKAROUND_STATE,
+ nonSteam ? (integration.originalExecutable || originalExecutable) : null,
+ integration.commandTokenAdded,
+ );
+ if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state");
+ return makeSnapshot(integration.snapshot, finalized, nonSteam);
+ } catch (error) {
+ let rollbackSucceeded = true;
+ if (integration) {
+ try {
+ await removeWrapperIntegration(
+ Number(appId),
+ nonSteam,
+ wrapperPath,
+ nonSteam ? (integration?.originalExecutable || originalExecutable || undefined) : undefined,
+ integration?.commandTokenAdded ?? false,
+ );
+ } catch {
+ // Leave the owned integration in place rather than guessing at cleanup.
+ rollbackSucceeded = false;
+ }
+ }
+ if (rollbackSucceeded) {
+ const removed = await removeWorkaroundState(appId);
+ if (!removed.success) throw new Error(removed.error || "Could not roll back workaround state");
+ }
+ throw error;
+ }
}
export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWorkarounds {
@@ -49,8 +146,25 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
const pendingSliderUpdate = useRef<PendingSliderUpdate | null>(null);
const numericAppId = Number(appId);
- const applySnapshot = useCallback((steam: SteamLaunchOptionsSnapshot) => {
- setSnapshot(makeSnapshot(steam));
+ const loadSnapshot = useCallback(async () => {
+ const [result, steam] = await Promise.all([
+ getWorkaroundState(appId),
+ readSteamLaunchOptions(numericAppId, nonSteam),
+ ]);
+ if (!result.success) throw new Error(result.error || "Could not read workaround state");
+ if (!result.state) {
+ return adoptWorkaroundState(
+ appId,
+ nonSteam,
+ steam,
+ result.wrapper_path || getDefaultWrapperPath(),
+ );
+ }
+ return makeSnapshot(steam, result, nonSteam);
+ }, [appId, nonSteam, numericAppId]);
+
+ const applySnapshot = useCallback((next: WorkaroundSnapshot) => {
+ setSnapshot(next);
setStatus("ready");
setError(null);
}, []);
@@ -59,65 +173,79 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
setStatus("loading");
setError(null);
try {
- applySnapshot(await readSteamLaunchOptions(numericAppId, nonSteam));
+ applySnapshot(await loadSnapshot());
} catch (refreshError) {
const nextError = asError(refreshError);
setStatus("error");
setError(nextError.message);
}
- }, [applySnapshot, nonSteam, numericAppId]);
+ }, [applySnapshot, loadSnapshot]);
useEffect(() => {
let active = true;
setStatus("loading");
setSnapshot(null);
setError(null);
-
- const handleSnapshot = (nextSnapshot: SteamLaunchOptionsSnapshot) => {
- if (!active) return;
- applySnapshot(nextSnapshot);
- };
- const handleSubscriptionError = (subscriptionError: Error) => {
- if (!active) return;
- setStatus("error");
- setError(subscriptionError.message);
- };
-
let unsubscribe = () => {};
try {
unsubscribe = subscribeSteamLaunchOptions(
numericAppId,
nonSteam,
- handleSnapshot,
- handleSubscriptionError,
+ (steam) => {
+ if (!active) return;
+ setSnapshot((current) => current ? {
+ ...current,
+ steam,
+ integrationInstalled: integrationIsInstalled(steam, nonSteam, current.wrapperPath),
+ } : current);
+ },
+ (subscriptionError) => {
+ if (!active) return;
+ setStatus("error");
+ setError(subscriptionError.message);
+ },
);
} catch (subscriptionError) {
- handleSubscriptionError(asError(subscriptionError));
+ if (active) {
+ setStatus("error");
+ setError(asError(subscriptionError).message);
+ }
}
-
- void readSteamLaunchOptions(numericAppId, nonSteam)
- .then((nextSnapshot) => {
- if (active) applySnapshot(nextSnapshot);
- })
+ void loadSnapshot()
+ .then((next) => { if (active) applySnapshot(next); })
.catch((readError) => {
- if (active) handleSubscriptionError(asError(readError));
+ if (active) {
+ setStatus("error");
+ setError(asError(readError).message);
+ }
});
-
return () => {
active = false;
unsubscribe();
};
- }, [applySnapshot, nonSteam, numericAppId]);
+ }, [applySnapshot, loadSnapshot, nonSteam, numericAppId]);
const persistUpdate = useCallback(async (field: WorkaroundField, value: boolean | number): Promise<boolean> => {
+ const current = snapshot;
+ if (!current) return false;
setError(null);
+ const nextState = { ...current.state, [field]: value } as WorkaroundState;
try {
- const nextSnapshot = await updateSteamLaunchOptions(
- numericAppId,
- nonSteam,
- (options) => applyWorkaroundChange(options, field, value),
+ const result = await setWorkaroundState(
+ appId,
+ nextState,
+ current.shortcutExe ?? null,
+ current.commandTokenAdded,
);
- applySnapshot(nextSnapshot);
+ if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state");
+ applySnapshot({
+ ...current,
+ state: result.state,
+ wrapperPath: result.wrapper_path || current.wrapperPath,
+ wrapperOwned: result.wrapper_owned === true,
+ shortcutExe: result.shortcut_exe,
+ commandTokenAdded: result.command_token_added === true,
+ });
return true;
} catch (updateError) {
const nextError = asError(updateError);
@@ -126,14 +254,13 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
showErrorToast("Workaround update failed", nextError.message);
return false;
}
- }, [applySnapshot, nonSteam, numericAppId]);
+ }, [appId, applySnapshot, snapshot]);
const flushSliderUpdate = useCallback(async (): Promise<boolean> => {
const pending = pendingSliderUpdate.current;
if (!pending) return true;
-
pendingSliderUpdate.current = null;
- window.clearTimeout(pending.timer);
+ clearTimeout(pending.timer);
const success = await persistUpdate("dxvkFrameRate", pending.value);
pending.waiters.forEach((resolve) => resolve(success));
return success;
@@ -143,30 +270,25 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
if (field === "dxvkFrameRate") {
setError(null);
return new Promise<boolean>((resolve) => {
- const pending = pendingSliderUpdate.current ?? { timer: 0, value: 0, waiters: [] };
+ const pending = pendingSliderUpdate.current || { timer: 0, value: 0, waiters: [] };
window.clearTimeout(pending.timer);
pending.value = Number(value);
pending.waiters.push(resolve);
- pending.timer = window.setTimeout(() => {
- void flushSliderUpdate();
- }, SLIDER_DEBOUNCE_MS);
+ pending.timer = window.setTimeout(() => { void flushSliderUpdate(); }, SLIDER_DEBOUNCE_MS);
pendingSliderUpdate.current = pending;
});
}
-
const sliderSuccess = await flushSliderUpdate();
if (!sliderSuccess) return false;
return persistUpdate(field, value);
}, [flushSliderUpdate, persistUpdate]);
- useEffect(() => {
- return () => {
- const pending = pendingSliderUpdate.current;
- if (!pending) return;
- window.clearTimeout(pending.timer);
- pendingSliderUpdate.current = null;
- pending.waiters.forEach((resolve) => resolve(false));
- };
+ useEffect(() => () => {
+ const pending = pendingSliderUpdate.current;
+ if (!pending) return;
+ window.clearTimeout(pending.timer);
+ pendingSliderUpdate.current = null;
+ pending.waiters.forEach((resolve) => resolve(false));
}, [numericAppId, nonSteam]);
return useMemo(() => ({ status, snapshot, refresh, update, error }), [error, refresh, snapshot, status, update]);