Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
67 lines
1.8 KiB
TypeScript
67 lines
1.8 KiB
TypeScript
import type { TranscodeTaskFn } from './video-transcode-queue'
|
|
|
|
export const VIDEO_TRANSCODE_RECOVERY_INTERVAL_MS = 60_000
|
|
|
|
interface RecoverySchedulerOptions {
|
|
scan?: () => Promise<number>
|
|
intervalMs?: number
|
|
onError?: (error: unknown) => void
|
|
}
|
|
|
|
export interface VideoTranscodeRecoveryScheduler {
|
|
initial: Promise<number>
|
|
runNow: () => Promise<number>
|
|
stop: () => void
|
|
}
|
|
|
|
/**
|
|
* 建立不会重叠的恢复调度器。一次扫描失败只记录错误,下一次周期仍会重新尝试。
|
|
*/
|
|
export function createVideoTranscodeRecoveryScheduler(
|
|
options: RecoverySchedulerOptions = {},
|
|
): VideoTranscodeRecoveryScheduler {
|
|
const scan = options.scan || (async () => {
|
|
const [{ transcodeQueue, VIDEO_MATERIAL_TYPES }, { executeTranscodeTask }] = await Promise.all([
|
|
import('./video-transcode-queue'),
|
|
import('./video-transcode-task'),
|
|
])
|
|
return transcodeQueue.runRecoveryScan(
|
|
executeTranscodeTask as TranscodeTaskFn,
|
|
{ requeuePending: true, videoTypes: VIDEO_MATERIAL_TYPES },
|
|
)
|
|
})
|
|
const onError = options.onError || ((error) => {
|
|
console.error('[VideoTranscodeRecovery] Recovery scan failed:', error)
|
|
})
|
|
let inFlight: Promise<number> | null = null
|
|
|
|
const runNow = (): Promise<number> => {
|
|
if (inFlight) {
|
|
return inFlight
|
|
}
|
|
inFlight = scan()
|
|
.catch((error) => {
|
|
onError(error)
|
|
throw error
|
|
})
|
|
.finally(() => {
|
|
inFlight = null
|
|
})
|
|
return inFlight
|
|
}
|
|
|
|
const initial = runNow()
|
|
const timer = setInterval(() => {
|
|
void runNow().catch(() => {
|
|
// onError 已记录;保留定时器,让下一轮继续重试。
|
|
})
|
|
}, options.intervalMs || VIDEO_TRANSCODE_RECOVERY_INTERVAL_MS)
|
|
;(timer as NodeJS.Timeout).unref?.()
|
|
|
|
return {
|
|
initial,
|
|
runNow,
|
|
stop: () => clearInterval(timer),
|
|
}
|
|
}
|