refactor(extension): class-based command registry + modular src layout
Testing / remote-protocol-compat (0.9.5) (push) Successful in 45s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Testing / test (push) Successful in 52s

Restructure the MV3 background worker from a monolithic core.ts/index.ts
into a class-based command architecture. Behavior is identical — the 83
registered commands dispatch byte-for-byte the same as before.

Structure
- One class per command group, each extending CommandGroup and exporting a
  `commands` map keyed by the full command id ("tabs.close"). Groups:
  Navigation, TabsMutation, TabsQuery, Groups, Windows, Dom (dom/extract/
  page), BrowserData (storage/cookies), Session (session/clients + autosave
  + lazy-tab activation), Perf (perf + jobs.status/cancel), Extension.
- CommandRegistry merges the group maps (throws on duplicate ids), routes
  background specs to JobManager and paginates array results via
  makePagedData. JobManager owns the job map + lifecycle. NativeConnection
  owns the native-port lifecycle and the inbound message router.
- index.ts is now thin wiring: JobManager -> ctx -> assembleRegistry ->
  onActivated -> NativeConnection.start().
- Infra classes live in classes/ (PascalCase, file = class name); command
  groups in commands/; shared helpers split out of core.ts into core/
  (errors, throttle, scripting, tab-helpers, group-helpers, storage); all
  types moved into types/ (json, jobs, session, tabs, messages,
  command-args) behind a barrel.

DRY cleanup
- resolveTabUrl(tabId) and assertScriptableUrl(url, action) collapse the
  tab/URL-guard boilerplate duplicated across dom.ts and browser-data.ts.
- processInBatches() centralizes the throttled, cancellable batch loop
  shared by tabs.close, group.close and tabs.merge_windows.
- captureCurrentSession() dedups the snapshot-and-signature block shared by
  session.save and the autosave path.
- DomArgs type alias replaces 21 inline ContentArgs & { tabId? } copies.
- Drop fetchTabHtml's redundant retry loop (executeScript already retries
  transient frame/tab errors), a dead tabInfo import, and two stale
  comments referencing a removed asArgs helper.

Type safety & tests
- Full noImplicitAny; no `any`/`unknown` annotations remain in src.
- JS unit-test harness using node --test + node:assert (zero new deps),
  bundled via the existing esbuild. Covers JobManager retention/lifecycle
  and the autosave listener-wiring/debounce with an in-memory chrome mock.
- The structural pytest checks track the new file homes and the centralized
  processInBatches helper.

Verification: npm run check:extension green (tsc + esbuild 84.5kb +
node --check + 18 JS tests); uv run pytest -q -> 409 passed, 105 skipped.
No version bump.
This commit is contained in:
2026-06-11 00:33:00 +02:00
parent d2f2a99f3d
commit ba01be1c5d
41 changed files with 2522 additions and 1589 deletions
@@ -0,0 +1,54 @@
import { normalizeGroupColor } from '../core';
import type { SessionTab, StoredSession } from '../types';
export function buildSessionSnapshot(tabs: chrome.tabs.Tab[], groups: chrome.tabGroups.TabGroup[]): SessionTab[] {
const groupById = new Map(groups.map(group => [group.id, group]));
return tabs
.filter(tab => Boolean(tab.url || tab.pendingUrl))
.sort((a, b) => (a.windowId - b.windowId) || (a.index - b.index))
.map(tab => {
const entry: SessionTab = { url: tab.url || tab.pendingUrl };
if (tab.pinned) entry.pinned = true;
if (tab.groupId >= 0) {
const group = groupById.get(tab.groupId);
entry.group = {
key: `${tab.windowId}:${tab.groupId}`,
title: group?.title || "",
color: normalizeGroupColor(group?.color),
collapsed: Boolean(group?.collapsed),
};
}
return entry;
});
}
/**
* Snapshot the current windows/tabs/groups into a ready-to-store session plus
* its change-detection signature. Shared by session.save and the autosave path.
*/
export async function captureCurrentSession(): Promise<{ session: StoredSession; signature: string; tabCount: number }> {
const tabs = await chrome.tabs.query({});
const groups = await chrome.tabGroups.query({});
const sessionTabs = buildSessionSnapshot(tabs, groups);
const signature = sessionSignature(sessionTabs);
const session: StoredSession = {
tabs: sessionTabs,
urls: sessionTabs.map(tab => tab.url),
savedAt: Date.now(),
signature,
};
return { session, signature, tabCount: sessionTabs.length };
}
export function sessionSignature(sessionTabs: SessionTab[]) {
return JSON.stringify(sessionTabs.map(tab => ({
url: tab.url,
pinned: Boolean(tab.pinned),
group: tab.group ? {
key: tab.group.key || "",
title: tab.group.title || "",
color: normalizeGroupColor(tab.group.color),
collapsed: Boolean(tab.group.collapsed),
} : null,
})));
}