fix(tabs): server-authoritative tab sync across browser sessions

Changed syncTabsFromServer to replace local tabs with server tabs
instead of merging (union). This prevents tabs closed in browser A
from being resurrected when browser B refreshes. Also added server
sync to the iframe module landing path which was localStorage-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-24 10:53:24 -07:00
parent 4722aca065
commit 21b38e7297
1 changed files with 47 additions and 14 deletions

View File

@ -871,7 +871,7 @@ export function renderShell(opts: ShellOptions): string {
if (_tabChannel) _tabChannel.close();
});
// Fetch tabs from server for authenticated users (merge with localStorage)
// Fetch tabs from server for authenticated users (server-authoritative)
(function syncTabsFromServer() {
try {
const raw = localStorage.getItem('encryptid_session');
@ -888,21 +888,17 @@ export function renderShell(opts: ShellOptions): string {
saveTabs();
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.filter(t => !_closedModuleIds.has(t.moduleId));
for (const [mid, lt] of localMap) {
if (!serverMap.has(mid) && !_closedModuleIds.has(mid)) merged.push(lt);
}
merged.forEach((l, i) => { l.order = i; });
layers = merged;
// Ensure current module is present
// Server-authoritative: adopt server tabs exactly.
// Only skip tabs the user closed in THIS session (prevents
// close-then-immediate-resurrect before save propagates).
var serverTabs = data.tabs.filter(t => !_closedModuleIds.has(t.moduleId));
serverTabs.forEach((l, i) => { l.order = i; });
layers = serverTabs;
// Ensure the currently navigated module is present
if (!layers.find(l => l.moduleId === currentModuleId)) {
layers.push(makeLayer(currentModuleId, layers.length));
}
tabBar.setLayers(layers);
reconcileRemoteLayers(layers);
saveTabs();
})
.catch(() => {});
@ -1455,7 +1451,44 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
tabBar.setLayers(layers);
tabBar.setAttribute('active', 'layer-' + currentModuleId);
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
function saveTabs() { localStorage.setItem(TABS_KEY, JSON.stringify(layers)); }
function saveTabs() {
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
try {
var raw = localStorage.getItem('encryptid_session');
if (raw) {
var session = JSON.parse(raw);
if (session?.accessToken) {
fetch('/api/user/tabs/' + encodeURIComponent(spaceSlug), {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
body: JSON.stringify({ tabs: layers }),
}).catch(function(){});
}
}
} catch(e) {}
}
// Sync tabs from server (server-authoritative)
(function() {
try {
var raw = localStorage.getItem('encryptid_session');
if (!raw) return;
var session = JSON.parse(raw);
if (!session?.accessToken) return;
fetch('/api/user/tabs/' + encodeURIComponent(spaceSlug), {
headers: { 'Authorization': 'Bearer ' + session.accessToken },
})
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(data) {
if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; }
layers = data.tabs;
layers.forEach(function(l, i) { l.order = i; });
if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length));
tabBar.setLayers(layers);
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
})
.catch(function(){});
} catch(e) {}
})();
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });