fix(shell): prevent remote sync from wiping all tabs and showing dashboard
reconcileRemoteLayers() could wipe all local tabs when Automerge sync or BroadcastChannel delivered empty layer data (CRDT initial state, sync race). This caused clicking tabs to show an infinite-loading dashboard. Now: empty remote layers are rejected when an active module exists, and the current module always stays in the tab list. Also adds 10s fetch timeout to TabCache.fetchAndInject() to prevent infinite loading spinners on slow/failed module fetches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
50edf06900
commit
433833da0c
|
|
@ -844,6 +844,15 @@ export function renderShell(opts: ShellOptions): string {
|
|||
|
||||
// Reconcile remote layer changes (shared by BroadcastChannel + Automerge)
|
||||
function reconcileRemoteLayers(remoteLayers) {
|
||||
// Guard: never let remote sync wipe all tabs when we have an active module.
|
||||
// Empty remote layers indicate a CRDT initial state or sync race, not
|
||||
// an intentional "close everything" action.
|
||||
if (remoteLayers.length === 0 && currentModuleId) {
|
||||
// Keep local layers intact — remote has nothing useful
|
||||
tabBar.setLayers(layers);
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = new Set(layers.map(l => l.moduleId));
|
||||
const next = new Set(remoteLayers.map(l => l.moduleId));
|
||||
|
||||
|
|
@ -854,34 +863,13 @@ export function renderShell(opts: ShellOptions): string {
|
|||
|
||||
layers = deduplicateLayers(remoteLayers);
|
||||
|
||||
// Always ensure the current module stays in the tab list
|
||||
if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) {
|
||||
// Active tab was closed remotely — switch to nearest
|
||||
if (layers.length > 0) {
|
||||
const nearest = layers[layers.length - 1];
|
||||
currentModuleId = nearest.moduleId;
|
||||
layers.push(makeLayer(currentModuleId, layers.length));
|
||||
}
|
||||
|
||||
tabBar.setLayers(layers);
|
||||
tabBar.setAttribute('active', 'layer-' + currentModuleId);
|
||||
if (tabCache) {
|
||||
tabCache.switchTo(currentModuleId).then(ok => {
|
||||
if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, currentModuleId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No tabs left — show dashboard
|
||||
const dashboard = document.querySelector('rstack-user-dashboard');
|
||||
if (dashboard && dashboard.setOpenTabs) dashboard.setOpenTabs([...layers]);
|
||||
tabBar.setLayers([]);
|
||||
tabBar.setAttribute('active', '');
|
||||
currentModuleId = '';
|
||||
if (tabCache) tabCache.hideAllPanes();
|
||||
if (dashboard) { dashboard.style.display = ''; if (dashboard.refresh) dashboard.refresh(); }
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.remove('canvas-layout');
|
||||
history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', '/' + spaceSlug);
|
||||
}
|
||||
} else {
|
||||
tabBar.setLayers(layers);
|
||||
}
|
||||
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ export class TabCache {
|
|||
try {
|
||||
const resp = await fetch(fetchUrl, {
|
||||
headers: { "Accept": "text/html" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
loadingPane.remove();
|
||||
|
|
|
|||
Loading…
Reference in New Issue