summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYao Chung Hu <30311066+FlashyReese@users.noreply.github.com>2026-10-26 16:08:24 -0600
committerGitHub <noreply@github.com>2026-10-26 18:08:24 -0400
commit6301508037b30489c42ee9ea8acee97cdea76297 (patch)
tree8a26cb6f89b18ff7ccef4f90ab91f48354f32049
parent8395766dcda9eca166da5e8b9e600d432d8f5607 (diff)
downloaddecky-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.json5
-rw-r--r--frontend/src/components/store/Store.tsx56
-rw-r--r--frontend/src/plugin-loader.tsx23
-rw-r--r--frontend/src/store.tsx38
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) {