feat: Add hardened error handling and retries (#2451)
- Closes #1830 - Closes #2099
This commit is contained in:
committed by
dermotduffy
parent
4bc787e2b7
commit
47bcce93d3
@@ -1,7 +1,6 @@
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||
import { ViewDisplayMode } from '../../config/schema/common/display';
|
||||
import { AdvancedCameraCardConfig } from '../../config/schema/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { resolveViewName } from '../../view/utils/resolve-default';
|
||||
import { View, ViewParameters } from '../../view/view';
|
||||
import {
|
||||
@@ -182,16 +181,16 @@ export class ViewFactory {
|
||||
if (options?.failSafe && !doesViewRequireCamera(defaultViewName)) {
|
||||
return { viewName: defaultViewName, cameraID: null };
|
||||
}
|
||||
const cameraID = this._api.getCameraManager().getStore().getDefaultCameraID();
|
||||
if (options?.failSafe) {
|
||||
return {
|
||||
viewName: defaultViewName,
|
||||
cameraID: this._api.getCameraManager().getStore().getDefaultCameraID(),
|
||||
cameraID,
|
||||
};
|
||||
}
|
||||
throw new ViewIncompatible(localize('error.no_supported_cameras'), {
|
||||
throw new ViewIncompatible({
|
||||
view: viewName,
|
||||
camera: null,
|
||||
default_view: defaultViewName,
|
||||
camera: cameraID,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,10 +220,9 @@ export class ViewFactory {
|
||||
?.getCapabilities()
|
||||
?.getRawCapabilities();
|
||||
|
||||
throw new ViewIncompatible(localize('error.no_supported_camera'), {
|
||||
throw new ViewIncompatible({
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
default_view: defaultViewName,
|
||||
...(capabilities && { camera_capabilities: capabilities }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { format } from 'date-fns';
|
||||
import { createNotificationFromError } from '../../components-lib/notification/factory';
|
||||
import { homeAssistantGetSignedURLIfNecessary } from '../../ha/sign-path';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { AdvancedCameraCardError } from '../../types';
|
||||
@@ -37,7 +38,15 @@ export class ViewItemManager {
|
||||
try {
|
||||
await this._download(item);
|
||||
} catch (error: unknown) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(error);
|
||||
/* istanbul ignore if: catch always provides a non-null error -- @preserve */
|
||||
if (error == null) {
|
||||
return false;
|
||||
}
|
||||
this._api.getNotificationManager().setNotification(
|
||||
createNotificationFromError(error, {
|
||||
heading: { text: localize('error.download_failed'), icon: 'mdi:download-off' },
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -75,4 +75,8 @@ export interface ViewManagerInterface {
|
||||
hasMajorMediaChange(oldView?: View | null, newView?: View | null): boolean;
|
||||
}
|
||||
|
||||
export class ViewIncompatible extends AdvancedCameraCardError {}
|
||||
export class ViewIncompatible extends AdvancedCameraCardError {
|
||||
constructor(context?: unknown) {
|
||||
super('', context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ViewContext } from 'view';
|
||||
import { log } from '../../utils/debug';
|
||||
import { getStreamCameraID } from '../../utils/substream';
|
||||
import { View } from '../../view/view';
|
||||
import { getViewTargetID } from '../../view/target-id';
|
||||
import { InitializationAspect } from '../initialization-manager';
|
||||
import { CardViewAPI } from '../types';
|
||||
import { ViewFactory } from './factory';
|
||||
@@ -111,19 +112,39 @@ export class ViewManager implements ViewManagerInterface {
|
||||
baseView: this._view,
|
||||
...options,
|
||||
});
|
||||
// A non-throwing factory call clears any prior view_incompatible /
|
||||
// media_query state — ensures a previously-dismissed mid-session popup
|
||||
// does not linger invisibly and re-pop on the next evaluation cycle,
|
||||
// and that a stale media_query failure from an abandoned gallery /
|
||||
// viewer doesn't follow the user into an unrelated view.
|
||||
this._api.getIssueManager().reset('view_incompatible');
|
||||
this._api.getIssueManager().reset('media_query');
|
||||
} catch (e) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||
if (!this._view) {
|
||||
view = this._getFailSafeView(viewFactoryFunc);
|
||||
}
|
||||
this._api.getIssueManager().trigger('view_incompatible', { error: e });
|
||||
}
|
||||
if (view) {
|
||||
this._setView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private _markViewLoadingQuery(view: View, index: number): View {
|
||||
return view.mergeInContext({ loading: { query: index } });
|
||||
private _getFailSafeView(
|
||||
viewFactoryFunc: (options?: ViewFactoryOptions) => View | null,
|
||||
): View | null {
|
||||
try {
|
||||
return viewFactoryFunc({ baseView: null, failSafe: true });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private _markViewAsNotLoadingQuery(view: View): View {
|
||||
return view.removeContextProperty('loading', 'query');
|
||||
|
||||
private _markViewLoadingQuery(view: View, index: number): void {
|
||||
view.mergeInContext({ loading: { query: index } });
|
||||
}
|
||||
private _markViewAsNotLoadingQuery(view: View): void {
|
||||
view.removeContextProperty('loading', 'query');
|
||||
}
|
||||
|
||||
private _isAllowedToSetView(): boolean {
|
||||
@@ -162,8 +183,16 @@ export class ViewManager implements ViewManagerInterface {
|
||||
...options?.params,
|
||||
},
|
||||
});
|
||||
this._api.getIssueManager().reset('view_incompatible');
|
||||
// A new query is about to run, so any stale media_query error from a
|
||||
// previous attempt is no longer meaningful. If this new query also
|
||||
// fails, it will re-trigger below.
|
||||
this._api.getIssueManager().reset('media_query');
|
||||
} catch (e) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||
if (!this._view) {
|
||||
initialView = this._getFailSafeView(viewFactoryFunc);
|
||||
}
|
||||
this._api.getIssueManager().trigger('view_incompatible', { error: e });
|
||||
}
|
||||
|
||||
if (!initialView) {
|
||||
@@ -205,13 +234,23 @@ export class ViewManager implements ViewManagerInterface {
|
||||
// the contrary, small changes such as the user zooming in are fine to
|
||||
// merge into the resultant view.
|
||||
if (this._view.context?.loading?.query === loadingIndex) {
|
||||
this._setView(this._markViewAsNotLoadingQuery(this._view.clone()));
|
||||
const view = this._view.clone();
|
||||
this._markViewAsNotLoadingQuery(view);
|
||||
this._setView(view);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(error);
|
||||
// Clear the loading flag before surfacing the error. Otherwise the
|
||||
// view stays marked in-flight and components (gallery, viewer) keep
|
||||
// rendering "Awaiting media" on top of the error notification.
|
||||
if (this._view?.context?.loading?.query === loadingIndex) {
|
||||
const view = this._view.clone();
|
||||
this._markViewAsNotLoadingQuery(view);
|
||||
this._setView(view);
|
||||
}
|
||||
this._api.getIssueManager().trigger('media_query', { error });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,6 +260,8 @@ export class ViewManager implements ViewManagerInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
this._api.getIssueManager().reset('media_query');
|
||||
|
||||
const newView = this._view.clone();
|
||||
if (this._view.context?.loading?.query === loadingIndex) {
|
||||
this._markViewAsNotLoadingQuery(newView);
|
||||
@@ -279,7 +320,7 @@ export class ViewManager implements ViewManagerInterface {
|
||||
);
|
||||
}
|
||||
|
||||
public initialize = async (): Promise<boolean> => {
|
||||
public initialize = async (): Promise<void> => {
|
||||
// If the query string contains a view related action, we don't set any view
|
||||
// here and allow that action to be triggered by the next call of to execute
|
||||
// query actions (called at least once per render cycle).
|
||||
@@ -289,7 +330,6 @@ export class ViewManager implements ViewManagerInterface {
|
||||
// query is answered.
|
||||
this.setViewDefaultWithNewQuery({ failSafe: true });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _setView(view: Readonly<View> | null): void {
|
||||
@@ -312,13 +352,13 @@ export class ViewManager implements ViewManagerInterface {
|
||||
this._api.getCardElementManager().scrollReset();
|
||||
}
|
||||
|
||||
this._api.getMessageManager().reset();
|
||||
this._api.getStyleManager().setExpandedMode();
|
||||
|
||||
this._api.getConditionStateManager()?.setState({
|
||||
view: view?.view,
|
||||
camera: view?.camera ?? undefined,
|
||||
displayMode: view?.displayMode ?? undefined,
|
||||
targetID: view ? getViewTargetID(view) ?? undefined : undefined,
|
||||
});
|
||||
|
||||
this._api.getCardElementManager().update();
|
||||
|
||||
Reference in New Issue
Block a user