/
mentoster
/
vk-stilus
Обзор
Документация
Войти
/
mentoster
/
vk-stilus
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
3
CI/CD
Аналитика
Безопасность
master
src/background/tab-controller.ts
212 строк
8 KB
mentoster
refactor(background): type privileged lifecycle owners
20 июл 2026, 22:02
20 июл 2026, 22:02
7c5dffb
Код
Авторство
О чём код?
import { verifyInvariant } from '../core/invariant.js'; import { err, ok } from '../core/result.js'; import type { BackgroundBrowserApi, BackgroundResult, BackgroundTab, ModeToggleOwner, PageAction, PageActionId, PageActionTarget, } from './background-types.js'; import { PAGE_ACTIONS } from './page-actions.js'; const TAB_CONTEXT = Object.freeze({ owner: 'TabController' }); const MAX_MATCHED_TABS = 1_000; const MAX_URL_PATTERNS = 16; const VK_HOME = 'https://vk.com'; /** @assertion-exception Pure tab error factory owns no lifecycle state. */ function tabError(code: string, message: string, context: unknown = null) { return Object.freeze({ code, message, context }); } /** @assertion-exception Pure tab shape guard owns no lifecycle state. */ function isBackgroundTab(value: unknown): value is BackgroundTab { return value !== null && typeof value === 'object' && !Array.isArray(value); } /** @assertion-exception Pure bounded tab normalization owns no lifecycle state. */ function normalizedTabs(value: unknown): readonly BackgroundTab[] { return Object.freeze( Array.isArray(value) ? value.filter(isBackgroundTab).slice(0, MAX_MATCHED_TABS) : [], ); } /** @assertion-exception Pure bounded active-tab selection owns no lifecycle state. */ function activeTab(tabs: readonly BackgroundTab[]): BackgroundTab | null { for (let index = 0; index < tabs.length && index < MAX_MATCHED_TABS; index += 1) { const tab = tabs[index]; if (tab?.active) return tab; } return null; } /** @assertion-exception Pure bounded tab-id selection owns no lifecycle state. */ function tabById(tabs: readonly BackgroundTab[], tabId: number): BackgroundTab | null { for (let index = 0; index < tabs.length && index < MAX_MATCHED_TABS; index += 1) { if (tabs[index]?.id === tabId) return tabs[index] ?? null; } return null; } /** @assertion-exception Pure bounded target selection owns no lifecycle state. */ function selectedTabs( tabs: readonly BackgroundTab[], target: PageActionTarget, ): readonly BackgroundTab[] { if (target.scope === 'all') return tabs.slice(0, MAX_MATCHED_TABS); if (target.scope === 'first') return tabs[0] ? [tabs[0]] : []; const tab = Number.isInteger(target.tabId) ? tabById(tabs, Number(target.tabId)) : null; return tab ? [tab] : []; } /** @assertion-exception Pure page-action lookup owns no lifecycle state. */ function pageAction(actionId: string): PageAction | null { return actionId in PAGE_ACTIONS ? PAGE_ACTIONS[actionId as PageActionId] : null; } /** * @classdesc Owns VK tab discovery, action-click routing, and finite reviewed page-script execution. * @responsibility Query bounded VK tabs, activate/create tabs, toggle mode, and execute fixed page actions. * @nonresponsibility Generate arbitrary scripts, persist mode, render icons, or authorize commands. * @trustBoundary Validates browser tab/script Results and ignores malformed tabs without widening scope. * @lifecycle Construct once after manifest validation and use throughout one background lifecycle. * @invariant URL patterns and page-action registry are frozen; no more than 1,000 tabs are processed. * @resourceBudget One tab query and at most 1,000 checked script calls per explicit operation. * @sideEffects Queries/creates/activates VK tabs and executes reviewed fixed functions in MAIN world. */ export class TabController { browserApi: BackgroundBrowserApi; urlPatterns: readonly string[]; toggleMode: ModeToggleOwner['toggle']; constructor( options: Readonly<{ browserApi: BackgroundBrowserApi; urlPatterns: readonly string[]; toggleMode: ModeToggleOwner['toggle']; }>, ) { this.browserApi = options.browserApi; this.urlPatterns = Object.freeze([...options.urlPatterns].slice(0, MAX_URL_PATTERNS)); this.toggleMode = options.toggleMode; const patternsValid = verifyInvariant( this.urlPatterns.length > 0 && this.urlPatterns.length <= MAX_URL_PATTERNS, 'tabs.construct.patterns', TAB_CONTEXT, ); const toggleValid = verifyInvariant( typeof this.toggleMode === 'function', 'tabs.construct.toggle', TAB_CONTEXT, ); if (!patternsValid.ok || !toggleValid.ok) throw new TypeError('TabController unavailable'); } /** @assertion-exception Checked tab query wrapper owns no reusable mutable state. */ async queryVkTabs(): Promise<BackgroundResult<readonly BackgroundTab[]>> { const result = await this.browserApi.queryTabs({ url: this.urlPatterns }); return result.ok ? ok(normalizedTabs(result.value)) : result; } /** @assertion-exception Finite action-click dispatcher delegates only fixed operations. */ async routeActionClick( tabs: readonly BackgroundTab[], ): Promise<BackgroundResult<Readonly<Record<string, unknown>>>> { if (tabs.length === 0) { const created = await this.browserApi.createTab({ url: VK_HOME, index: 0 }); return created.ok ? ok(Object.freeze({ action: 'created' })) : created; } if (activeTab(tabs)) { const toggled = await this.toggleMode(); return toggled.ok ? ok(Object.freeze({ action: 'toggled', state: toggled.value })) : toggled; } const first = tabs[0]; if (!first || !Number.isInteger(first.id)) { return err(tabError('TAB_ID_INVALID', 'VK tab identifier is invalid')); } const activated = await this.browserApi.updateTab(Number(first.id), { active: true }); return activated.ok ? ok(Object.freeze({ action: 'activated', tabId: first.id })) : activated; } async handleActionClick(): Promise<BackgroundResult<Readonly<Record<string, unknown>>>> { const queried = await this.queryVkTabs(); if (!queried.ok) return queried; const bounded = verifyInvariant( queried.value.length <= MAX_MATCHED_TABS, 'tabs.click.bounded', TAB_CONTEXT, ); const frozen = verifyInvariant( Object.isFrozen(queried.value), 'tabs.click.frozen', TAB_CONTEXT, ); if (!bounded.ok) return err(bounded.error); if (!frozen.ok) return err(frozen.error); return this.routeActionClick(queried.value); } /** @assertion-exception Checked bounded execution loop owns no reusable mutable state. */ async executeSelected( action: PageAction, argument: unknown, tabs: readonly BackgroundTab[], ): Promise<BackgroundResult<Readonly<{ executed: number }>>> { let executedCount = 0; for (let index = 0; index < tabs.length && index < MAX_MATCHED_TABS; index += 1) { const tab = tabs[index]; const tabId = Number(tab?.id); if (!Number.isInteger(tabId)) continue; const executed = await this.browserApi.executeScript({ target: { tabId }, func: action, args: [argument], world: 'MAIN', }); if (!executed.ok) { return err(tabError('PAGE_ACTION_FAILED', 'reviewed page action failed', executed.error)); } executedCount += 1; } return ok(Object.freeze({ executed: executedCount })); } async executePageAction( actionId: string, argument: unknown, target: PageActionTarget, ): Promise<BackgroundResult<Readonly<Record<string, unknown>>>> { const action = pageAction(actionId); if (!action) { return err(tabError('PAGE_ACTION_DENIED', 'page action is not allowlisted', actionId)); } const queried = await this.queryVkTabs(); if (!queried.ok) return queried; const tabs = selectedTabs(queried.value, target); const actionIsStable = pageAction(actionId) === action; const actionStable = verifyInvariant( actionIsStable, 'tabs.execute.action-stable', TAB_CONTEXT, ); const selectionBounded = verifyInvariant( tabs.length <= MAX_MATCHED_TABS, 'tabs.execute.selection-bounded', TAB_CONTEXT, ); if (!actionStable.ok) return err(actionStable.error); if (!selectionBounded.ok) return err(selectionBounded.error); const executed = await this.executeSelected(action, argument, tabs); return executed.ok ? ok(Object.freeze({ ...executed.value, actionId })) : executed; } }