- Shared per-job trigger controller (apps/shared) coalesces duplicate clicks for the same profile+job inside a mounted client while letting unrelated jobs run independently; the backend durable claim remains authoritative across windows/processes. - Two-phase feedback everywhere: the action stays disabled/spinning while the request is in flight and the terminal success/error is reported once, after the HTTP response — no premature success toast (Web), matching the Desktop info notification. - Desktop keeps the 24h trigger timeout for the synchronous long operation and fences stale profile/list responses and unmounted surfaces; the sidebar trigger button shows a spinner while busy.
41 lines
1.1 KiB
TypeScript
41 lines
1.1 KiB
TypeScript
export interface CronTriggerRunResult<T> {
|
|
started: boolean
|
|
value: T | null
|
|
}
|
|
|
|
export interface CronTriggerController {
|
|
isRunning(key: string): boolean
|
|
run<T>(key: string, action: () => Promise<T>, onStarted?: () => void): Promise<CronTriggerRunResult<T>>
|
|
}
|
|
|
|
// This is an interaction guard for one mounted UI surface. Cross-window and
|
|
// cross-process exclusion remains the backend's responsibility via its durable
|
|
// cron claim; a renderer-local Set must never be treated as the execution lock.
|
|
export function createCronTriggerController(
|
|
onRunningChange: (key: string, running: boolean) => void = () => undefined
|
|
): CronTriggerController {
|
|
const running = new Set<string>()
|
|
|
|
return {
|
|
isRunning: key => running.has(key),
|
|
async run<T>(key: string, action: () => Promise<T>, onStarted?: () => void) {
|
|
if (running.has(key)) {
|
|
return { started: false, value: null }
|
|
}
|
|
|
|
running.add(key)
|
|
|
|
try {
|
|
onRunningChange(key, true)
|
|
|
|
onStarted?.()
|
|
|
|
return { started: true, value: await action() }
|
|
} finally {
|
|
running.delete(key)
|
|
onRunningChange(key, false)
|
|
}
|
|
}
|
|
}
|
|
}
|