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:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user