feat(sessions): restored tabs start asleep; suppress restore refresh echo

Background tabs created during session restore are discarded right after
creation so a large session does not load every page at once (the first
active tab stays loaded; Chrome reloads a discarded tab on activation and
natively refuses to discard an active tab, so no active-guard IPC is
needed). The restore runs under the refresh-suppression window so the
tabs.create/discard event echo does not cause a redundant post-restore
refresh (the explicit renderDashboard call is unaffected).

Adds behavioral probes asserting both restore modes discard every
background tab, keep the first active tab loaded, and leave the restored
id list intact (removing either discard call turns them red).
This commit is contained in:
Leexunhuan743
2026-08-17 07:45:02 +08:00
parent 1f50d7a6c4
commit b88577def9
2 changed files with 121 additions and 18 deletions
+79
View File
@@ -1150,3 +1150,82 @@ test('ensureWindowsKeepLastTab never empties a window (real implementation)', ()
// A single-tab window whose only tab is in the close set is protected.
assert.deepEqual(fn([{ id: 9, windowId: 9, active: false }], [9]), []);
});
// ---------------------------------------------------------------------------
// Behavioral: restore-created tabs start ASLEEP (discard-on-restore)
// ---------------------------------------------------------------------------
test('openSavedTabsInCurrentWindow discards every background tab but keeps the first active one loaded', async () => {
const fn = new Function(`
${extractFn(runtimeJs, 'discardTab')}
${extractFn(runtimeJs, 'openSavedTabsInCurrentWindow')}
return openSavedTabsInCurrentWindow;
`)();
const discarded = [];
globalThis.getCurrentWindowId = async () => 501;
globalThis.chrome = {
windows: { getCurrent: async () => ({ id: 501 }) },
tabs: {
create: async (opts) => ({
id: opts.url === 'https://a.test' ? 1001 : opts.url === 'https://b.test' ? 1002 : 1003,
url: opts.url,
active: !!opts.active,
windowId: opts.windowId ?? 501,
}),
discard: async (id) => { discarded.push(Number(id)); },
query: async () => [],
},
};
const result = await fn([
{ url: 'https://a.test' },
{ url: 'https://b.test' },
{ url: 'https://c.test' },
]);
// The first tab is created active and must NOT be discarded; the two
// background tabs are discarded right after creation (restored tabs start
// asleep so a large session does not load every page at once).
assert.deepEqual(discarded, [1002, 1003]);
// The restored id list is unaffected by the discards.
assert.deepEqual(result.restoredTabs.map(t => t.id), [1001, 1002, 1003]);
delete globalThis.getCurrentWindowId;
delete globalThis.chrome;
});
test('openSavedTabsInNewWindow discards every background tab of the restored session', async () => {
const fn = new Function(`
${extractFn(runtimeJs, 'discardTab')}
${extractFn(runtimeJs, 'openSavedTabsInNewWindow')}
return openSavedTabsInNewWindow;
`)();
const discarded = [];
globalThis.chrome = {
windows: {
create: async (opts) => ({ id: 502, tabs: [{ id: 2001, url: opts.url, active: true }] }),
},
tabs: {
create: async (opts) => ({
id: opts.url === 'https://a.test' ? 2001 : opts.url === 'https://b.test' ? 2002 : 2003,
url: opts.url,
active: !!opts.active,
windowId: opts.windowId ?? 502,
}),
discard: async (id) => { discarded.push(Number(id)); },
query: async () => [],
},
};
const result = await fn([
{ url: 'https://a.test' },
{ url: 'https://b.test' },
{ url: 'https://c.test' },
]);
// The first tab comes from windows.create (active) and is never discarded.
assert.deepEqual(discarded, [2002, 2003]);
assert.deepEqual(result.restoredTabs.map(t => t.id), [2001, 2002, 2003]);
delete globalThis.chrome;
});
+42 -18
View File
@@ -2052,6 +2052,14 @@ async function openSavedTabsInCurrentWindow(tabs = []) {
url: tab.url,
active: false,
});
// Restored tabs start ASLEEP: tabs.create loads the URL immediately, which
// spikes CPU/memory when a session holds many tabs. Discard right after
// creation — the tab keeps its URL and Chrome reloads it on activation.
// Fire-and-forget: discardTab catches its own failures and the restored id
// list below is unaffected (discard keeps the tab id and group).
if (createdTab?.id != null) {
discardTab(Number(createdTab.id));
}
restoredTabs.push({
id: createdTab.id,
url: tab.url,
@@ -2093,6 +2101,14 @@ async function openSavedTabsInNewWindow(tabs = []) {
url: tab.url,
active: false,
});
// Restored tabs start ASLEEP: tabs.create loads the URL immediately, which
// spikes CPU/memory when a session holds many tabs. Discard right after
// creation — the tab keeps its URL and Chrome reloads it on activation.
// Fire-and-forget: discardTab catches its own failures and the restored id
// list below is unaffected (discard keeps the tab id and group).
if (createdTab?.id != null) {
discardTab(Number(createdTab.id));
}
restoredTabs.push({
id: createdTab.id,
url: tab.url,
@@ -2125,26 +2141,34 @@ async function restoreSavedTabSession(sessionId) {
}
const restoreMode = runtimeGetSavedSessionRestoreMode ? runtimeGetSavedSessionRestoreMode() : 'new-window';
const { restoredTabs, windowId } = restoreMode === 'current-window'
? await openSavedTabsInCurrentWindow(session.tabs)
: await openSavedTabsInNewWindow(session.tabs);
const { state: nextSessionGroups, chromeGroupPlans } = runtimeCreateRestoredSessionGroups({
existingState: sessionGroupsState,
session,
restoredTabs,
now: new Date().toISOString(),
// Run under the refresh-suppression window: the burst of tabs.create and the
// deferred discard events echo back as tabs-changed → debounced renderDashboard
// calls. The restore already renders explicitly below, so suppressing that
// echo avoids a redundant post-restore refresh. The explicit renderDashboard
// call is unaffected — suppression only gates the event-driven refresh path.
return runWithSuppressedRefresh(async () => {
const { restoredTabs, windowId } = restoreMode === 'current-window'
? await openSavedTabsInCurrentWindow(session.tabs)
: await openSavedTabsInNewWindow(session.tabs);
const { state: nextSessionGroups, chromeGroupPlans } = runtimeCreateRestoredSessionGroups({
existingState: sessionGroupsState,
session,
restoredTabs,
now: new Date().toISOString(),
});
// Re-create native Chrome groups that were saved with the session: fresh
// groups with the recorded title/color, tabs in the recorded order.
await restoreChromeGroupsForSession(chromeGroupPlans, windowId);
await saveSessionGroups(nextSessionGroups);
await renderDashboard();
return {
restoredCount: restoredTabs.length,
windowId,
};
});
// Re-create native Chrome groups that were saved with the session: fresh
// groups with the recorded title/color, tabs in the recorded order.
await restoreChromeGroupsForSession(chromeGroupPlans, windowId);
await saveSessionGroups(nextSessionGroups);
await renderDashboard();
return {
restoredCount: restoredTabs.length,
windowId,
};
}
/**