fix(tabs): prevent closed tabs from reopening via server sync race
Two race conditions caused closed tabs to resurrect: 1. syncTabsFromServer() fetch completing after a local close, merging the stale server response back in 2. Debounced PUT killed by page navigation when closing the active tab, so the server never learned about the close Fix: track closed moduleIds per session to skip during merge, and flush server PUT with keepalive:true before navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ad22ed7482
commit
be81618b70
|
|
@ -540,23 +540,29 @@ export function renderShell(opts: ShellOptions): string {
|
|||
|
||||
// Helper: save current tab list to localStorage + server
|
||||
let _tabSaveTimer = null;
|
||||
function saveTabs() {
|
||||
// Track tabs closed this session so server merge doesn't resurrect them
|
||||
const _closedModuleIds = new Set();
|
||||
function saveTabs(immediate) {
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
// Debounced server save for authenticated users
|
||||
clearTimeout(_tabSaveTimer);
|
||||
_tabSaveTimer = setTimeout(() => {
|
||||
const doSave = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('encryptid_session');
|
||||
if (!raw) return;
|
||||
const session = JSON.parse(raw);
|
||||
if (!session?.accessToken) return;
|
||||
fetch('/api/user/tabs/' + encodeURIComponent(spaceSlug), {
|
||||
const url = '/api/user/tabs/' + encodeURIComponent(spaceSlug);
|
||||
const body = JSON.stringify({ tabs: layers });
|
||||
fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
|
||||
body: JSON.stringify({ tabs: layers }),
|
||||
body: body,
|
||||
keepalive: immediate, // survive page navigation when flushing immediately
|
||||
}).catch(() => {});
|
||||
} catch(e) {}
|
||||
}, 500);
|
||||
};
|
||||
if (immediate) { doSave(); } else { _tabSaveTimer = setTimeout(doSave, 500); }
|
||||
}
|
||||
|
||||
// Fetch tabs from server for authenticated users (merge with localStorage)
|
||||
|
|
@ -577,11 +583,12 @@ export function renderShell(opts: ShellOptions): string {
|
|||
return;
|
||||
}
|
||||
// Merge: union of moduleIds, server order wins for shared tabs
|
||||
// Skip tabs that were closed this session (prevents resurrection)
|
||||
const serverMap = new Map(data.tabs.map(t => [t.moduleId, t]));
|
||||
const localMap = new Map(layers.map(t => [t.moduleId, t]));
|
||||
const merged = [...data.tabs];
|
||||
const merged = data.tabs.filter(t => !_closedModuleIds.has(t.moduleId));
|
||||
for (const [mid, lt] of localMap) {
|
||||
if (!serverMap.has(mid)) merged.push(lt);
|
||||
if (!serverMap.has(mid) && !_closedModuleIds.has(mid)) merged.push(lt);
|
||||
}
|
||||
merged.forEach((l, i) => { l.order = i; });
|
||||
layers = merged;
|
||||
|
|
@ -659,6 +666,8 @@ export function renderShell(opts: ShellOptions): string {
|
|||
const wasActive = layerId === tabBar.getAttribute('active');
|
||||
tabBar.removeLayer(layerId);
|
||||
layers = layers.filter(l => l.id !== layerId);
|
||||
// Track closed tab so server merge doesn't resurrect it
|
||||
_closedModuleIds.add(closedModuleId);
|
||||
saveTabs();
|
||||
|
||||
// If this was a space layer with a persisted SpaceRef, clean it up
|
||||
|
|
@ -696,9 +705,13 @@ export function renderShell(opts: ShellOptions): string {
|
|||
currentModuleId = nextModuleId;
|
||||
if (tabCache) {
|
||||
tabCache.switchTo(nextModuleId).then(ok => {
|
||||
if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
|
||||
if (!ok) {
|
||||
saveTabs(true); // flush to server before navigation
|
||||
window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveTabs(true); // flush to server before navigation
|
||||
window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue