feat: Add hardened error handling and retries (#2451)
- Closes #1830 - Closes #2099
This commit is contained in:
committed by
dermotduffy
parent
4bc787e2b7
commit
47bcce93d3
@@ -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