diff options
| author | Yao Chung Hu <30311066+FlashyReese@users.noreply.github.com> | 2026-10-26 16:08:24 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-26 18:08:24 -0400 |
| commit | 6301508037b30489c42ee9ea8acee97cdea76297 (patch) | |
| tree | 8a26cb6f89b18ff7ccef4f90ab91f48354f32049 | |
| parent | 8395766dcda9eca166da5e8b9e600d432d8f5607 (diff) | |
| download | decky-loader-6301508037b30489c42ee9ea8acee97cdea76297.tar.gz decky-loader-6301508037b30489c42ee9ea8acee97cdea76297.zip | |
fix: handle plugin store load failures (#1001)HEADmainaa/v4-work
| -rw-r--r-- | backend/decky_loader/locales/en-US.json | 5 | ||||
| -rw-r--r-- | frontend/src/components/store/Store.tsx | 56 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 23 | ||||
| -rw-r--r-- | frontend/src/store.tsx | 38 |
4 files changed, 89 insertions, 33 deletions
diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 297cab05..e9c9117d 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -251,6 +251,11 @@ "start": "Initializing", "uninstalling_previous": "Uninstalling previous copy" }, + "errors": { + "load_failed": "Unable to load the plugin store. Please try again.", + "update_required": "Update Decky Loader to use the plugin store." + }, + "retry": "Retry", "store_contrib": { "desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.", "label": "Contributing" diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index be2248fd..cfa83aff 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -1,4 +1,5 @@ import { + DialogButton, Dropdown, DropdownOption, Focusable, @@ -13,7 +14,15 @@ import { useTranslation } from 'react-i18next'; import logo from '../../../assets/plugin_store.png'; import Logger from '../../logger'; -import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store'; +import { + SortDirections, + SortOptions, + Store, + StorePlugin, + StoreRequestError, + getPluginList, + getStore, +} from '../../store'; import { useDeckyState } from '../DeckyState'; import ExternalLink from '../ExternalLink'; import PluginCard from './PluginCard'; @@ -86,22 +95,40 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> // const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data); const [searchFieldValue, setSearchValue] = useState<string>(''); const [pluginList, setPluginList] = useState<StorePlugin[] | null>(null); + const [loadError, setLoadError] = useState<Error | null>(null); + const [retryCount, setRetryCount] = useState(0); const [isTesting, setIsTesting] = useState<boolean>(false); useEffect(() => { + const controller = new AbortController(); + setPluginList(null); + setPluginCount(null); + setLoadError(null); (async () => { - const res = await getPluginList(selectedSort[0], selectedSort[1]); - logger.debug('got data!', res); - setPluginList(res); - setPluginCount(res.length); + try { + const res = await getPluginList(selectedSort[0], selectedSort[1], controller.signal); + if (controller.signal.aborted) return; + logger.debug('got data!', res); + setPluginList(res); + setPluginCount(res.length); + } catch (error) { + if (controller.signal.aborted) return; + logger.error('Failed to load the plugin store', error); + setLoadError(error instanceof Error ? error : new Error(String(error))); + } })(); - }, [selectedSort]); + return () => controller.abort(); + }, [selectedSort, retryCount, setPluginCount]); useEffect(() => { (async () => { - const storeRes = await getStore(); - logger.debug(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`); - setIsTesting(storeRes === Store.Testing); + try { + const storeRes = await getStore(); + logger.debug(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`); + setIsTesting(storeRes === Store.Testing); + } catch (error) { + logger.error('Failed to read the store channel', error); + } })(); }, []); @@ -224,7 +251,16 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> </div> )} <div> - {!pluginList ? ( + {loadError ? ( + <div role="alert" style={{ padding: '20px', textAlign: 'center' }}> + <p> + {loadError instanceof StoreRequestError && loadError.status === 426 + ? t('Store.errors.update_required') + : t('Store.errors.load_failed')} + </p> + <DialogButton onClick={() => setRetryCount((count) => count + 1)}>{t('Store.retry')}</DialogButton> + </div> + ) : !pluginList ? ( <div style={{ height: '100%' }}> <SteamSpinner background="transparent" /> </div> diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 185d260a..81b96dcc 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -34,7 +34,7 @@ import { NotificationService } from './notification-service'; import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin'; import RouterHook from './router-hook'; import { deinitSteamFixes, initSteamFixes } from './steamfixes'; -import { checkForPluginUpdates } from './store'; +import { PluginUpdateMapping, checkForPluginUpdates } from './store'; import TabsHook from './tabs-hook'; import Toaster from './toaster'; import { getVersionInfo } from './updater'; @@ -274,19 +274,24 @@ class PluginLoader extends Logger { await this.notifyPluginUpdates(); } - public async checkPluginUpdates() { - const frozenPlugins = this.deckyState.publicState().frozenPlugins; + public async checkPluginUpdates(): Promise<PluginUpdateMapping | null> { + try { + const frozenPlugins = this.deckyState.publicState().frozenPlugins; - const updates = await checkForPluginUpdates( - this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)), - ); - this.deckyState.setUpdates(updates); - return updates; + const updates = await checkForPluginUpdates( + this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)), + ); + this.deckyState.setUpdates(updates); + return updates; + } catch (error) { + this.error('Failed to check for plugin updates', error); + return null; + } } public async notifyPluginUpdates() { const updates = await this.checkPluginUpdates(); - if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) { + if (updates && updates.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) { this.pluginUpdateToast && this.pluginUpdateToast.dismiss(); this.pluginUpdateToast = this.toaster.toast({ title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />, diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx index 55017a66..80bb860d 100644 --- a/frontend/src/store.tsx +++ b/frontend/src/store.tsx @@ -47,6 +47,13 @@ export interface PluginInstallRequest { // name: version export type PluginUpdateMapping = Map<string, StorePluginVersion>; +export class StoreRequestError extends Error { + constructor(public readonly status: number) { + super(`Plugin store request failed (HTTP ${status})`); + this.name = 'StoreRequestError'; + } +} + export async function getStore(): Promise<Store> { return await getSetting<Store>('store', Store.Default); } @@ -81,35 +88,38 @@ export async function getStoreBaseURL(): Promise<string> { export async function getPluginList( sort_by: SortOptions | null = null, sort_direction: SortDirections | null = null, + signal?: AbortSignal, ): Promise<StorePlugin[]> { - let version = await window.DeckyPluginLoader.updateVersion(); - let query: URLSearchParams | string = new URLSearchParams(); sort_by && query.set('sort_by', sort_by); sort_direction && query.set('sort_direction', sort_direction); query = '?' + String(query); - const storeURL = await getStoreBaseURL(); - - return fetch(storeURL + query, { - method: 'GET', - headers: { - 'X-Decky-Version': version.current, - }, - }).then((r) => r.json()); + const data = await getStoreData(query, signal); + if (!Array.isArray(data)) throw new Error('Plugin store returned an invalid plugin list'); + return data; } export async function getVersionList(): Promise<UpdateEndpointVersions> { - let version = await window.DeckyPluginLoader.updateVersion(); + const data = await getStoreData('/versions'); + if (!data || typeof data !== 'object' || Array.isArray(data)) { + throw new Error('Plugin store returned an invalid version list'); + } + return data as UpdateEndpointVersions; +} +async function getStoreData(path: string, signal?: AbortSignal): Promise<unknown> { + const version = await window.DeckyPluginLoader.updateVersion(); const storeURL = await getStoreBaseURL(); - - return fetch(storeURL + '/versions', { + const response = await fetch(storeURL + path, { method: 'GET', + signal, headers: { 'X-Decky-Version': version.current, }, - }).then((r) => r.json()); + }); + if (!response.ok) throw new StoreRequestError(response.status); + return response.json(); } export async function installFromURL(url: string) { |
