fix: stop the home screen's tmux scan in a hidden tab (release review)

The poll had no hidden-tab gate, and the local scan's 5 s cache sits under the
8 s interval, so a single tab never hit it: every tick was a full socket sweep
(1.1 s and 772 forks on a box with 212 sockets) for a list nobody was reading.

`_foreignPollTick()` is now the one gate for the first load, the timer and the
return to a visible tab. The `visibilitychange` listener is stored and removed
in `stopForeignPolling()`, since this runs on every trip to the home screen.

Checked in real chromium with document.hidden stubbed: 2 polls in 9.5 s visible,
0 in 17.5 s hidden, 1 on return, and a tab opened in the background makes none
until it is shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 08:41:15 +01:00
1 parent 37dfb73b1e
commit 9714ee79e8
3 files changed
+108 -9

No files matched your search

+85 -3
View File
@@ -41,7 +41,7 @@ function stubEl(): Record<string, unknown> {
}
/** The module is a classic script that mixes into CodemanApp.prototype. */
function loadApp(window: Record<string, unknown> = {}) {
function loadApp(window: Record<string, unknown> = {}, page: Record<string, unknown> = {}) {
const host = stubEl();
const ctx: Record<string, unknown> = {
console,
@@ -56,11 +56,13 @@ function loadApp(window: Record<string, unknown> = {}) {
clearInterval() {},
codemanT: null,
window,
document: {
document: Object.assign(page, {
createElement: () => stubEl(),
getElementById: (id: string) => (id === 'foreignSessions' ? host : null),
addEventListener() {},
},
removeEventListener() {},
...page,
}),
CodemanApp: function () {} as unknown,
};
vm.createContext(ctx);
@@ -74,6 +76,8 @@ function loadApp(window: Record<string, unknown> = {}) {
_allForeignRows(): Array<{ id: string }>;
renderAllForeignSessions(): void;
startForeignPolling(): void;
stopForeignPolling(): void;
_foreignPollTick(): void;
renderForeignSessions(el: unknown): void;
_foreignRow(row: object): { children: Array<{ children: Array<{ textContent: string }> }> };
};
@@ -161,6 +165,84 @@ describe('the wide scan is one-shot', () => {
});
});
describe('a hidden tab does not sweep tmux sockets', () => {
// One scan forks once per socket and pane, and a tab left on the home screen in the
// background has nobody reading the list: 772 forks a tick was measured on a real box.
function harness(hidden: boolean) {
const listeners = new Set<() => void>();
const page: Record<string, unknown> = {
hidden,
addEventListener(type: string, listener: () => void) {
if (type === 'visibilitychange') listeners.add(listener);
},
removeEventListener(type: string, listener: () => void) {
if (type === 'visibilitychange') listeners.delete(listener);
},
};
const app = loadApp({}, page);
const calls: string[] = [];
app._api = vi.fn(async (path: string) => {
calls.push(path);
return ok({ sessions: [LOCAL], notes: [], canScanWide: false });
});
return { app, calls, page, listeners };
}
it('makes no first load for a home screen that opened in the background', () => {
const { app, calls } = harness(true);
app.startForeignPolling();
expect(calls).toHaveLength(0);
});
it('skips the tick while hidden and runs it again once visible', () => {
const { app, calls, page } = harness(false);
app.startForeignPolling();
expect(calls).toHaveLength(1);
page.hidden = true;
app._foreignPollTick();
expect(calls).toHaveLength(1);
page.hidden = false;
app._foreignPollTick();
expect(calls).toHaveLength(2);
});
it('catches up the moment the tab comes back, without waiting out the interval', () => {
const { app, calls, page, listeners } = harness(true);
app.startForeignPolling();
expect(listeners.size).toBe(1);
page.hidden = false;
for (const listener of listeners) listener();
expect(calls).toEqual(['/api/mux/foreign']);
});
it('does not scan on the visibility event that hides the tab', () => {
const { app, calls, page, listeners } = harness(false);
app.startForeignPolling();
expect(listeners.size).toBe(1);
page.hidden = true;
for (const listener of listeners) listener();
expect(calls).toHaveLength(1);
});
it('holds one listener over repeated home screens and none after leaving', () => {
// showWelcome/hideWelcome run this on every trip, unlike a poller created once.
const { app, listeners } = harness(false);
for (let trip = 0; trip < 3; trip++) {
app.startForeignPolling();
app.startForeignPolling();
expect(listeners.size).toBe(1);
app.stopForeignPolling();
expect(listeners.size).toBe(0);
}
});
});
describe('an adoption failure reports what the server said', () => {
function adoptHarness(status: number, body: unknown) {
const app = loadApp();