feat(overlay-sync): wire OverlaySyncService into DI, IPC and renderer gating
This commit is contained in:
10
packages/application/ports/IAutomationEventPublisher.ts
Normal file
10
packages/application/ports/IAutomationEventPublisher.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type AutomationEvent = {
|
||||
actionId?: string
|
||||
type: 'panel-attached'|'modal-opened'|'action-started'|'action-complete'|'action-failed'|'panel-missing'
|
||||
timestamp: number
|
||||
payload?: any
|
||||
}
|
||||
|
||||
export interface IAutomationEventPublisher {
|
||||
publish(event: AutomationEvent): Promise<void>
|
||||
}
|
||||
7
packages/application/ports/IOverlaySyncPort.ts
Normal file
7
packages/application/ports/IOverlaySyncPort.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type OverlayAction = { id: string; label: string; meta?: Record<string, unknown>; timeoutMs?: number }
|
||||
export type ActionAck = { id: string; status: 'confirmed' | 'tentative' | 'failed'; reason?: string }
|
||||
|
||||
export interface IOverlaySyncPort {
|
||||
startAction(action: OverlayAction): Promise<ActionAck>
|
||||
cancelAction(actionId: string): Promise<void>
|
||||
}
|
||||
112
packages/application/services/OverlaySyncService.ts
Normal file
112
packages/application/services/OverlaySyncService.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { IOverlaySyncPort, OverlayAction, ActionAck } from '../ports/IOverlaySyncPort'
|
||||
import { IAutomationEventPublisher, AutomationEvent } from '../ports/IAutomationEventPublisher'
|
||||
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../infrastructure/adapters/IAutomationLifecycleEmitter'
|
||||
import { ILogger } from '../ports/ILogger'
|
||||
|
||||
type ConstructorArgs = {
|
||||
lifecycleEmitter: IAutomationLifecycleEmitter
|
||||
publisher: IAutomationEventPublisher
|
||||
logger: ILogger
|
||||
initialPanelWaitMs?: number
|
||||
maxPanelRetries?: number
|
||||
backoffFactor?: number
|
||||
defaultTimeoutMs?: number
|
||||
}
|
||||
|
||||
export class OverlaySyncService implements IOverlaySyncPort {
|
||||
private lifecycleEmitter: IAutomationLifecycleEmitter
|
||||
private publisher: IAutomationEventPublisher
|
||||
private logger: ILogger
|
||||
private initialPanelWaitMs: number
|
||||
private maxPanelRetries: number
|
||||
private backoffFactor: number
|
||||
private defaultTimeoutMs: number
|
||||
|
||||
constructor(args: ConstructorArgs) {
|
||||
this.lifecycleEmitter = args.lifecycleEmitter
|
||||
this.publisher = args.publisher
|
||||
this.logger = args.logger
|
||||
this.initialPanelWaitMs = args.initialPanelWaitMs ?? 500
|
||||
this.maxPanelRetries = args.maxPanelRetries ?? 3
|
||||
this.backoffFactor = args.backoffFactor ?? 2
|
||||
this.defaultTimeoutMs = args.defaultTimeoutMs ?? 5000
|
||||
}
|
||||
|
||||
async startAction(action: OverlayAction): Promise<ActionAck> {
|
||||
const timeoutMs = action.timeoutMs ?? this.defaultTimeoutMs
|
||||
const seenEvents: AutomationEvent[] = []
|
||||
let settled = false
|
||||
|
||||
const cb: LifecycleCallback = async (ev) => {
|
||||
seenEvents.push(ev)
|
||||
if (ev.type === 'action-started' && ev.actionId === action.id) {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
cleanup()
|
||||
resolveAck({ id: action.id, status: 'confirmed' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
try {
|
||||
this.lifecycleEmitter.offLifecycle(cb)
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let resolveAck: (ack: ActionAck) => void = () => {}
|
||||
const promise = new Promise<ActionAck>((resolve) => {
|
||||
resolveAck = resolve
|
||||
// subscribe
|
||||
try {
|
||||
this.lifecycleEmitter.onLifecycle(cb)
|
||||
} catch (e) {
|
||||
this.logger?.error?.('OverlaySyncService: failed to subscribe to lifecycleEmitter', e)
|
||||
}
|
||||
})
|
||||
|
||||
// publish overlay request (best-effort)
|
||||
try {
|
||||
this.publisher.publish({
|
||||
type: 'modal-opened',
|
||||
timestamp: Date.now(),
|
||||
payload: { actionId: action.id, label: action.label },
|
||||
actionId: action.id,
|
||||
} as AutomationEvent)
|
||||
} catch (e) {
|
||||
this.logger?.warn?.('OverlaySyncService: publisher.publish failed', e)
|
||||
}
|
||||
|
||||
// timeout handling
|
||||
const timeoutPromise = new Promise<ActionAck>((res) => {
|
||||
setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
cleanup()
|
||||
this.logger?.info?.('OverlaySyncService: timeout waiting for confirmation', { actionId: action.id, timeoutMs })
|
||||
// log recent events truncated
|
||||
const lastEvents = seenEvents.slice(-10)
|
||||
this.logger?.debug?.('OverlaySyncService: recent lifecycle events', lastEvents)
|
||||
res({ id: action.id, status: 'tentative', reason: 'timeout' })
|
||||
}
|
||||
}, timeoutMs)
|
||||
})
|
||||
|
||||
return Promise.race([promise, timeoutPromise])
|
||||
}
|
||||
|
||||
async cancelAction(actionId: string): Promise<void> {
|
||||
// best-effort: publish cancellation
|
||||
try {
|
||||
await this.publisher.publish({
|
||||
type: 'panel-missing',
|
||||
timestamp: Date.now(),
|
||||
actionId,
|
||||
} as AutomationEvent)
|
||||
} catch (e) {
|
||||
this.logger?.warn?.('OverlaySyncService: cancelAction publish failed', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user