ba01be1c5d
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.
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { CommandGroup } from './CommandGroup';
|
|
import type { CommandContext, CommandEntry, CommandSpec } from './CommandGroup';
|
|
import { NavigationCommands } from '../commands/navigation';
|
|
import { TabsMutationCommands } from '../commands/tabs';
|
|
import { TabsQueryCommands } from '../commands/tabs-query';
|
|
import { GroupsCommands } from '../commands/groups';
|
|
import { WindowsCommands } from '../commands/windows';
|
|
import { DomCommands } from '../commands/dom';
|
|
import { BrowserDataCommands } from '../commands/browser-data';
|
|
import { SessionCommands } from '../commands/session';
|
|
import { PerfCommands } from '../commands/perf';
|
|
import { ExtensionCommands } from '../commands/extension';
|
|
import type { CommandArgs, Serializable, DispatchArgs, PageRequest } from '../types';
|
|
|
|
function isCommandSpec(entry: CommandEntry): entry is CommandSpec {
|
|
return typeof entry !== "function";
|
|
}
|
|
|
|
export function makePagedData(items: Serializable[], page: PageRequest) {
|
|
const total = items.length;
|
|
const offset = Math.max(0, Number(page.offset) || 0);
|
|
const requestedLimit = Math.max(1, Number(page.limit) || 100);
|
|
const limit = Math.min(requestedLimit, 1000);
|
|
const end = Math.min(offset + limit, total);
|
|
return {
|
|
__browserCliPage: true,
|
|
items: items.slice(offset, end),
|
|
offset,
|
|
limit,
|
|
total,
|
|
nextOffset: end < total ? end : null,
|
|
};
|
|
}
|
|
|
|
export class CommandRegistry {
|
|
private readonly entries = new Map<string, CommandEntry>();
|
|
|
|
constructor(private readonly ctx: CommandContext) {}
|
|
|
|
/** Flattens a group's full-id-keyed commands into the registry map. */
|
|
register(group: CommandGroup): void {
|
|
for (const [command, entry] of Object.entries(group.commands)) {
|
|
if (this.entries.has(command)) {
|
|
throw new Error(`Duplicate command registration: ${command}`);
|
|
}
|
|
this.entries.set(command, entry);
|
|
}
|
|
}
|
|
|
|
private resolve(command: string): CommandEntry {
|
|
const entry = this.entries.get(command);
|
|
if (!entry) throw new Error(`Unknown command: ${command}`);
|
|
return entry;
|
|
}
|
|
|
|
async dispatch(command: string, args: DispatchArgs, opts: { background?: boolean; page?: PageRequest }): Promise<Serializable> {
|
|
const entry = this.resolve(command);
|
|
|
|
if (isCommandSpec(entry) && entry.background && opts.background) {
|
|
// Narrow the dynamic IPC dict to the handler's declared arg type at the
|
|
// call boundary.
|
|
return this.ctx.jobs.start(command, args, jobArgs => Promise.resolve(entry.run(jobArgs as CommandArgs)));
|
|
}
|
|
|
|
const run = isCommandSpec(entry) ? entry.run : entry;
|
|
let result = await run(args as CommandArgs);
|
|
if (opts.page && Array.isArray(result)) {
|
|
result = makePagedData(result, opts.page);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the registry and registers every command group. The SessionCommands
|
|
* instance is returned alongside because index.ts wires its lifecycle methods
|
|
* (chrome.tabs.onActivated → activateLazyTab) and NativeConnection references it
|
|
* for the clients.rename_profile reconnect side-effect.
|
|
*/
|
|
export function assembleRegistry(ctx: CommandContext): { registry: CommandRegistry; session: SessionCommands } {
|
|
const registry = new CommandRegistry(ctx);
|
|
const session = new SessionCommands(ctx);
|
|
registry.register(new NavigationCommands(ctx));
|
|
registry.register(new TabsMutationCommands(ctx));
|
|
registry.register(new TabsQueryCommands(ctx));
|
|
registry.register(new GroupsCommands(ctx));
|
|
registry.register(new WindowsCommands(ctx));
|
|
registry.register(new DomCommands(ctx));
|
|
registry.register(new BrowserDataCommands(ctx));
|
|
registry.register(session);
|
|
registry.register(new PerfCommands(ctx));
|
|
registry.register(new ExtensionCommands(ctx));
|
|
return { registry, session };
|
|
}
|