Skip to content

Commit f6f3519

Browse files
committed
fix(v2): auto-retry failed gallery windows
syncFetches only runs on viewport changes, so a window whose fetch failed once would leave up to WINDOW_SIZE (72) visible cards stranded as skeletons on a static viewport until the user scrolled or the gallery context changed. Since only visible windows are ever fetched, a failed window was always visible when it failed. fetchWindowAt now schedules a bounded, backing-off refetch (up to 3 attempts, 2s/4s/6s) on failure so a transient blip self-heals. Retry timers and attempt counts are cleared on success and aborted alongside in-flight requests on any gallery-context switch, so we never retry against a stale context. Generated-By: PostHog Code Task-Id: 6690877a-9c32-4c91-a78b-3d6fd3851fb7
1 parent 6d92713 commit f6f3519

1 file changed

Lines changed: 41 additions & 2 deletions

File tree

frontend/src/v2/stores/galleryRoms.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,31 @@ const WINDOW_SIZE = 72;
7070
// leave server work going for results we'll throw away.
7171
const inFlightControllers = new Map<string, AbortController>();
7272

73+
// A failed window is only refetched when `fetchWindowAt` is called for it
74+
// again, which for a static viewport (no scroll) never happens on its own.
75+
// So a transient failure would strand up to `WINDOW_SIZE` visible cards as
76+
// skeletons. To self-heal, a failed window schedules a bounded, backing-off
77+
// retry; `retryTimers` holds the pending timeouts (keyed by offset) and
78+
// `retryCounts` the attempts so far. Both are cleared by `abortAllInFlight`
79+
// on any gallery-context switch so we never retry against a stale context.
80+
const RETRY_BACKOFF_MS = 2000;
81+
const MAX_WINDOW_RETRIES = 3;
82+
const retryTimers = new Map<number, ReturnType<typeof setTimeout>>();
83+
const retryCounts = new Map<number, number>();
84+
85+
function clearRetry(offset: number) {
86+
const timer = retryTimers.get(offset);
87+
if (timer !== undefined) clearTimeout(timer);
88+
retryTimers.delete(offset);
89+
retryCounts.delete(offset);
90+
}
91+
7392
function abortAllInFlight() {
7493
for (const ctrl of inFlightControllers.values()) ctrl.abort();
7594
inFlightControllers.clear();
95+
for (const timer of retryTimers.values()) clearTimeout(timer);
96+
retryTimers.clear();
97+
retryCounts.clear();
7698
}
7799

78100
// Apply items to `byPosition` in row-sized batches with a rAF yield
@@ -466,15 +488,32 @@ export default defineStore("v2GalleryRoms", {
466488
);
467489

468490
this.loadedWindows.add(offset);
491+
// Recovered — drop any retry bookkeeping for this window.
492+
clearRetry(offset);
469493
} catch (err) {
470494
// An explicit abort isn't a failure — keep `failedWindows`
471495
// clean so the window is eligible to refetch under the new
472496
// gallery context without the UI flagging it as broken.
473497
if (axios.isCancel(err)) return;
474498
this.failedWindows.add(offset);
475-
// Surface in console; UI keeps the skeletons in place the
476-
// window can be retried by re-entering the row.
499+
// Surface in console; UI keeps the skeletons in place until the
500+
// retry below (or a viewport / gallery-state change) refetches.
477501
console.error("[v2GalleryRoms] window fetch failed", offset, err);
502+
// Self-heal a stranded viewport: schedule a bounded, backing-off
503+
// refetch so a transient blip doesn't leave the window's cards as
504+
// skeletons until the user happens to scroll.
505+
const attempts = retryCounts.get(offset) ?? 0;
506+
if (attempts < MAX_WINDOW_RETRIES && !retryTimers.has(offset)) {
507+
retryCounts.set(offset, attempts + 1);
508+
const timer = setTimeout(
509+
() => {
510+
retryTimers.delete(offset);
511+
void this.fetchWindowAt(offset);
512+
},
513+
RETRY_BACKOFF_MS * (attempts + 1),
514+
);
515+
retryTimers.set(offset, timer);
516+
}
478517
} finally {
479518
inFlightControllers.delete(ctrlKey);
480519
this.pendingWindows.delete(offset);

0 commit comments

Comments
 (0)