gongxiangp2/lib/video-transcode-recovery.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

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),
}
}