feat(overlay-sync): wire OverlaySyncService into DI, IPC and renderer gating

This commit is contained in:
2025-11-26 19:14:25 +01:00
parent d08f9e5264
commit 1d7c4f78d1
20 changed files with 611 additions and 561 deletions

View File

@@ -19,6 +19,9 @@ import type { IAutomationEngine } from '@/packages/application/ports/IAutomation
import type { IAuthenticationService } from '@/packages/application/ports/IAuthenticationService';
import type { ICheckoutConfirmationPort } from '@/packages/application/ports/ICheckoutConfirmationPort';
import type { ILogger } from '@/packages/application/ports/ILogger';
import type { IAutomationLifecycleEmitter } from '@/packages/infrastructure/adapters/IAutomationLifecycleEmitter';
import type { IOverlaySyncPort } from '@/packages/application/ports/IOverlaySyncPort';
import { OverlaySyncService } from '@/packages/application/services/OverlaySyncService';
export interface BrowserConnectionResult {
success: boolean;
@@ -183,7 +186,9 @@ export class DIContainer {
private confirmCheckoutUseCase: ConfirmCheckoutUseCase | null = null;
private automationMode: AutomationMode;
private browserModeConfigLoader: BrowserModeConfigLoader;
private overlaySyncService?: OverlaySyncService;
private initialized = false;
private constructor() {
// Initialize logger first - it's needed by other components
this.logger = createLogger();
@@ -194,11 +199,20 @@ export class DIContainer {
nodeEnv: process.env.NODE_ENV
});
const config = loadAutomationConfig();
// Initialize browser mode config loader as singleton
// Defer heavy initialization that may touch Electron/app paths until first use.
// Keep BrowserModeConfigLoader available immediately so callers can inspect it.
this.browserModeConfigLoader = new BrowserModeConfigLoader();
}
/**
* Lazily perform initialization that may access Electron APIs or filesystem.
* Called on first demand by methods that require the heavy components.
*/
private ensureInitialized(): void {
if (this.initialized) return;
const config = loadAutomationConfig();
this.sessionRepository = new InMemorySessionRepository();
this.browserAutomation = createBrowserAutomationAdapter(
config.mode,
@@ -221,13 +235,19 @@ export class DIContainer {
this.checkAuthenticationUseCase = new CheckAuthenticationUseCase(authService);
this.initiateLoginUseCase = new InitiateLoginUseCase(authService);
this.clearSessionUseCase = new ClearSessionUseCase(authService);
} else {
this.checkAuthenticationUseCase = null;
this.initiateLoginUseCase = null;
this.clearSessionUseCase = null;
}
this.logger.info('DIContainer initialized', {
automationMode: config.mode,
sessionRepositoryType: 'InMemorySessionRepository',
browserAutomationType: this.getBrowserAutomationType(config.mode)
});
this.initialized = true;
}
private getBrowserAutomationType(mode: AutomationMode): string {
@@ -249,14 +269,17 @@ export class DIContainer {
}
public getStartAutomationUseCase(): StartAutomationSessionUseCase {
this.ensureInitialized();
return this.startAutomationUseCase;
}
public getSessionRepository(): ISessionRepository {
this.ensureInitialized();
return this.sessionRepository;
}
public getAutomationEngine(): IAutomationEngine {
this.ensureInitialized();
return this.automationEngine;
}
@@ -265,6 +288,7 @@ export class DIContainer {
}
public getBrowserAutomation(): IScreenAutomation {
this.ensureInitialized();
return this.browserAutomation;
}
@@ -273,18 +297,22 @@ export class DIContainer {
}
public getCheckAuthenticationUseCase(): CheckAuthenticationUseCase | null {
this.ensureInitialized();
return this.checkAuthenticationUseCase;
}
public getInitiateLoginUseCase(): InitiateLoginUseCase | null {
this.ensureInitialized();
return this.initiateLoginUseCase;
}
public getClearSessionUseCase(): ClearSessionUseCase | null {
this.ensureInitialized();
return this.clearSessionUseCase;
}
public getAuthenticationService(): IAuthenticationService | null {
this.ensureInitialized();
if (this.browserAutomation instanceof PlaywrightAutomationAdapter) {
return this.browserAutomation as IAuthenticationService;
}
@@ -294,6 +322,7 @@ export class DIContainer {
public setConfirmCheckoutUseCase(
checkoutConfirmationPort: ICheckoutConfirmationPort
): void {
this.ensureInitialized();
// Create ConfirmCheckoutUseCase with checkout service from browser automation
// and the provided confirmation port
this.confirmCheckoutUseCase = new ConfirmCheckoutUseCase(
@@ -303,6 +332,7 @@ export class DIContainer {
}
public getConfirmCheckoutUseCase(): ConfirmCheckoutUseCase | null {
this.ensureInitialized();
return this.confirmCheckoutUseCase;
}
@@ -312,6 +342,7 @@ export class DIContainer {
* In test mode, returns success immediately (no connection needed).
*/
public async initializeBrowserConnection(): Promise<BrowserConnectionResult> {
this.ensureInitialized();
this.logger.info('Initializing automation connection', { mode: this.automationMode });
if (this.automationMode === 'production' || this.automationMode === 'development') {
@@ -343,8 +374,9 @@ export class DIContainer {
* Should be called when the application is closing.
*/
public async shutdown(): Promise<void> {
this.ensureInitialized();
this.logger.info('DIContainer shutting down');
if (this.browserAutomation && 'disconnect' in this.browserAutomation) {
try {
await (this.browserAutomation as PlaywrightAutomationAdapter).disconnect();
@@ -353,7 +385,7 @@ export class DIContainer {
this.logger.error('Error disconnecting automation adapter', error instanceof Error ? error : new Error('Unknown error'));
}
}
this.logger.info('DIContainer shutdown complete');
}
@@ -365,6 +397,30 @@ export class DIContainer {
return this.browserModeConfigLoader;
}
public getOverlaySyncPort(): IOverlaySyncPort {
this.ensureInitialized();
if (!this.overlaySyncService) {
// Use the browser automation adapter as the lifecycle emitter when available.
const lifecycleEmitter = this.browserAutomation as unknown as IAutomationLifecycleEmitter;
// Lightweight in-process publisher (best-effort no-op). The ipc handlers will forward lifecycle events to renderer.
const publisher = {
publish: async (_event: any) => {
try {
this.logger.debug?.('OverlaySyncPublisher.publish', _event);
} catch {
// swallow
}
}
} as any;
this.overlaySyncService = new OverlaySyncService({
lifecycleEmitter,
publisher,
logger: this.logger
});
}
return this.overlaySyncService;
}
/**
* Recreate browser automation and related use-cases from the current
* BrowserModeConfigLoader state. This allows runtime changes to the
@@ -372,27 +428,28 @@ export class DIContainer {
* restarting the whole process.
*/
public refreshBrowserAutomation(): void {
this.ensureInitialized();
const config = loadAutomationConfig();
// Recreate browser automation adapter using current loader state
this.browserAutomation = createBrowserAutomationAdapter(
config.mode,
this.logger,
this.browserModeConfigLoader
);
// Recreate automation engine and start use case to pick up new adapter
this.automationEngine = new MockAutomationEngineAdapter(
this.browserAutomation,
this.sessionRepository
);
this.startAutomationUseCase = new StartAutomationSessionUseCase(
this.automationEngine,
this.browserAutomation,
this.sessionRepository
);
// Recreate authentication use-cases if adapter supports them, otherwise clear
if (this.browserAutomation instanceof PlaywrightAutomationAdapter) {
const authService = this.browserAutomation as IAuthenticationService;
@@ -404,7 +461,7 @@ export class DIContainer {
this.initiateLoginUseCase = null;
this.clearSessionUseCase = null;
}
this.logger.info('Browser automation refreshed from updated BrowserModeConfigLoader', {
browserMode: this.browserModeConfigLoader.load().mode
});