fix: Improve media_query retry behavior (#2603)
This commit is contained in:
@@ -187,9 +187,21 @@ export class IssueManager {
|
||||
|
||||
private _scheduleRetryIfNeeded(): void {
|
||||
if (!this._stateManager.needsRetry()) {
|
||||
// No issue still has a failing problem, so reset the backoff: a future
|
||||
// failure should start again from the base delay.
|
||||
this._retryTimer.reset();
|
||||
return;
|
||||
}
|
||||
if (!this._stateManager.canRetryNow()) {
|
||||
// An issue still has a failing problem but cannot retry right now. Cancel
|
||||
// the pending timer so it does not fire a retry the issue cannot take --
|
||||
// but do not reset(), which is reserved for the resolved case above.
|
||||
// Consequently no retry is scheduled here; scheduling resumes on a later
|
||||
// evaluate() (any condition-state change re-runs this), once the issue
|
||||
// reports it can retry again or has resolved.
|
||||
this._retryTimer.cancel();
|
||||
return;
|
||||
}
|
||||
if (this._retryTimer.isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,27 +17,78 @@ export class MediaQueryIssue extends AbstractErrorIssue {
|
||||
|
||||
private _api: CardIssueManagerAPI;
|
||||
|
||||
// True while a retry query is in flight. The error is retained throughout (so
|
||||
// the issue stays visible), but no further retry is scheduled until this
|
||||
// attempt resolves via a success (reset) or a fresh failure (trigger).
|
||||
private _retrying = false;
|
||||
|
||||
constructor(api: CardIssueManagerAPI) {
|
||||
super();
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
// A fresh outcome (success or failure) ends any in-flight retry.
|
||||
public trigger(context: { error: unknown }): void {
|
||||
this._retrying = false;
|
||||
super.trigger(context);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._retrying = false;
|
||||
super.reset();
|
||||
}
|
||||
|
||||
// There is still a failed query to retry as long as an error is present --
|
||||
// including while a retry is in flight -- so the backoff keeps escalating
|
||||
// across attempts instead of restarting each time one is dispatched.
|
||||
public needsRetry(): boolean {
|
||||
return this._error !== null;
|
||||
}
|
||||
|
||||
// A retry can be dispatched only when one is not already in flight; otherwise
|
||||
// the manager waits for the in-flight attempt to succeed or fail.
|
||||
public canRetryNow(): boolean {
|
||||
return this._error !== null && !this._retrying;
|
||||
}
|
||||
|
||||
public retry(): boolean {
|
||||
if (this._error === null) {
|
||||
return false;
|
||||
}
|
||||
this._error = null;
|
||||
void this._api.getViewManager().setViewByParametersWithNewQuery();
|
||||
|
||||
// An attempt is already running (only a forced retry reaches here while in
|
||||
// flight; scheduled ones are gated by canRetryNow). Do not dispatch a second
|
||||
// concurrent query -- the retry the caller wants is already happening.
|
||||
if (this._retrying) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mark the attempt in flight and keep the error set: it is cleared only on
|
||||
// confirmed success, or replaced on the next failure. The 'retry' intent
|
||||
// tells the view manager not to clear it up front (see
|
||||
// setViewByParametersWithNewQuery). The error must stay visible until it is
|
||||
// resolved, without any "flicker"/resetting.
|
||||
this._retrying = true;
|
||||
this._runRetryQuery().catch(() => {});
|
||||
|
||||
// Exclusive retry. No other issue should attempt to retry until the next
|
||||
// evaluation cycle, when we'll know if this was successful.
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _runRetryQuery(): Promise<void> {
|
||||
try {
|
||||
await this._api
|
||||
.getViewManager()
|
||||
.setViewByParametersWithNewQuery({ intent: 'retry' });
|
||||
} finally {
|
||||
// The attempt has settled. On the normal paths a success (reset) or a
|
||||
// failure (trigger) has already cleared this flag; the query is not
|
||||
// guaranteed to report either, so clear it here as a fallback.
|
||||
this._retrying = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getNotification(): Notification | null {
|
||||
return this.getIssue()?.notification ?? null;
|
||||
}
|
||||
|
||||
@@ -114,13 +114,17 @@ export class IssueStateManager implements IssueReadOnlyState {
|
||||
return [...this._issues.values()].some((issue) => issue.needsRetry?.());
|
||||
}
|
||||
|
||||
public canRetryNow(): boolean {
|
||||
return [...this._issues.values()].some((issue) => this._canRetryNow(issue));
|
||||
}
|
||||
|
||||
public retry(key?: IssueKey, force?: boolean): void {
|
||||
const issues = key
|
||||
? [this._issues.get(key)].filter(isTruthy)
|
||||
: [...this._issues.values()];
|
||||
|
||||
for (const issue of issues) {
|
||||
if (!force && !issue.needsRetry?.()) {
|
||||
if (!force && !this._canRetryNow(issue)) {
|
||||
continue;
|
||||
}
|
||||
if (issue.retry?.()) {
|
||||
@@ -162,6 +166,12 @@ export class IssueStateManager implements IssueReadOnlyState {
|
||||
// Private helpers.
|
||||
// =========================================================================
|
||||
|
||||
// An issue that does not implement canRetryNow falls back to needsRetry, so
|
||||
// issues with no in-flight concept can be retried whenever they need one.
|
||||
private _canRetryNow(issue: Issue): boolean {
|
||||
return issue.canRetryNow?.() ?? issue.needsRetry?.() ?? false;
|
||||
}
|
||||
|
||||
private _logIfNew(issue: Issue): void {
|
||||
const description = issue.getIssue();
|
||||
if (!description) {
|
||||
|
||||
@@ -72,10 +72,16 @@ export interface Issue {
|
||||
// depends on transient state (e.g. no current error to show).
|
||||
getNotification?(): Notification | null;
|
||||
|
||||
// Whether this issue wants the manager to schedule a retry. Gates
|
||||
// scheduled retries; user-initiated (forced) retries bypass this check.
|
||||
// Whether this issue has an unresolved problem that needs retry. The manager
|
||||
// keeps the backoff counter alive while this is true and clears it once the
|
||||
// problem resolves. Stays true across an in-flight retry (see canRetryNow).
|
||||
needsRetry?(): boolean;
|
||||
|
||||
// Whether a retry can run right now. False while one is already in flight, so
|
||||
// the manager waits without resetting the backoff. Forced (user-initiated)
|
||||
// retries bypass it. Defaults to needsRetry() when not implemented.
|
||||
canRetryNow?(): boolean;
|
||||
|
||||
// Called by the manager when a retry is due. Returns true to stop the retry
|
||||
// loop (exclusive), false to allow subsequent issues to also retry.
|
||||
retry?(): boolean;
|
||||
|
||||
@@ -50,6 +50,12 @@ export interface ViewFactoryOptions {
|
||||
// user-facing locks (e.g. an active call ending).
|
||||
force?: boolean;
|
||||
|
||||
// Why this view change is being made. 'retry' marks a re-run of a previously
|
||||
// failed query and, unlike a navigation, does not clear the existing error(s)
|
||||
// up front: it persists until this attempt resolves. Defaults to
|
||||
// 'navigation'.
|
||||
intent?: 'navigation' | 'retry';
|
||||
|
||||
// Options for the query executor that control how a query is executed and the
|
||||
// result selected.
|
||||
queryExecutorOptions?: QueryExecutorOptions;
|
||||
|
||||
@@ -218,10 +218,6 @@ export class ViewManager implements ViewManagerInterface {
|
||||
},
|
||||
});
|
||||
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) {
|
||||
if (!this._view) {
|
||||
initialView = this._getFailSafeView(viewFactoryFunc);
|
||||
@@ -233,6 +229,15 @@ export class ViewManager implements ViewManagerInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
// A new query is about to run and is allowed to commit, so any stale
|
||||
// media_query error from a previous attempt is no longer meaningful (on
|
||||
// failure it re-triggers below). A retry is the exception: it re-runs the
|
||||
// same failing query, so its error must persist until this attempt resolves
|
||||
// rather than being cleared here and re-appearing on the next failure.
|
||||
if (options?.intent !== 'retry') {
|
||||
this._api.getIssueManager().reset('media_query');
|
||||
}
|
||||
|
||||
if (this._view && this._shouldAdoptQueryAndResults(initialView)) {
|
||||
initialView.query = this._view.query;
|
||||
initialView.queryResults = this._view.queryResults;
|
||||
|
||||
Reference in New Issue
Block a user