112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
} |