fix(shell): prevent infinite loop when rapidly switching tabs
Abort previous in-flight fetch when switchTo() is called again, and guard the shell's fallback navigation against stale callbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
06a58f1bba
commit
c2d7e8b238
|
|
@ -926,7 +926,13 @@ export function renderShell(opts: ShellOptions): string {
|
|||
const sp = document.querySelector('rstack-space-settings');
|
||||
if (sp) sp.setAttribute('module-id', moduleId);
|
||||
if (tabCache) {
|
||||
const switchId = moduleId; // capture for staleness check
|
||||
tabCache.switchTo(moduleId).then(ok => {
|
||||
// If user already clicked a different tab, don't navigate for this one
|
||||
if (currentModuleId !== switchId) {
|
||||
console.log('[shell] switchTo result:', ok, 'for', switchId, '(stale, user switched to', currentModuleId + ')');
|
||||
return;
|
||||
}
|
||||
console.log('[shell] switchTo result:', ok, 'for', moduleId);
|
||||
if (ok) {
|
||||
tabBar.setAttribute('active', layerId);
|
||||
|
|
@ -936,6 +942,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
window.location.href = url;
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (currentModuleId !== switchId) return; // stale
|
||||
console.error('[shell] switchTo error:', err);
|
||||
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export class TabCache {
|
|||
private currentModuleId: string;
|
||||
private spaceSlug: string;
|
||||
private panes = new Map<string, HTMLElement>();
|
||||
private fetchController: AbortController | null = null;
|
||||
|
||||
constructor(spaceSlug: string, moduleId: string) {
|
||||
this.spaceSlug = spaceSlug;
|
||||
|
|
@ -100,6 +101,12 @@ export class TabCache {
|
|||
|
||||
/** Switch to a module tab within the current space. Returns true if handled client-side. */
|
||||
async switchTo(moduleId: string): Promise<boolean> {
|
||||
// Abort any in-flight fetch from a previous switchTo call
|
||||
if (this.fetchController) {
|
||||
this.fetchController.abort();
|
||||
this.fetchController = null;
|
||||
}
|
||||
|
||||
const key = this.paneKey(this.spaceSlug, moduleId);
|
||||
if (moduleId === this.currentModuleId && this.panes.has(key)) {
|
||||
console.log("[TabCache] switchTo", moduleId, "→ already current + cached");
|
||||
|
|
@ -199,18 +206,25 @@ export class TabCache {
|
|||
this.hideAllPanes();
|
||||
app.appendChild(loadingPane);
|
||||
|
||||
// Create an abort controller that also times out after 15s
|
||||
const controller = new AbortController();
|
||||
this.fetchController = controller;
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15_000);
|
||||
|
||||
try {
|
||||
const resp = await fetch(fetchUrl, {
|
||||
headers: { "Accept": "text/html" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
clearTimeout(timeoutId);
|
||||
console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fetchUrl);
|
||||
loadingPane.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
const html = await resp.text();
|
||||
clearTimeout(timeoutId);
|
||||
console.log("[TabCache] fetchAndInject: got", html.length, "bytes for", moduleId);
|
||||
const content = this.extractContent(html);
|
||||
if (!content) {
|
||||
|
|
@ -247,8 +261,16 @@ export class TabCache {
|
|||
console.log("[TabCache] fetchAndInject: SUCCESS for", moduleId);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err);
|
||||
clearTimeout(timeoutId);
|
||||
loadingPane.remove();
|
||||
// If aborted because the user switched to a different tab, return
|
||||
// "true" so the shell doesn't trigger a fallback full-page navigation
|
||||
// for a tab the user no longer wants.
|
||||
if (controller.signal.aborted && this.fetchController !== controller) {
|
||||
console.log("[TabCache] fetchAndInject: aborted (superseded) for", moduleId);
|
||||
return true;
|
||||
}
|
||||
console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue