import { ensureDatabase } from "./bootstrap"; import { DomainError, runRetentionMaintenance, type RetentionMaintenanceResult } from "./repository"; import { ACQUIRE_RETENTION_JOB_SQL, COMPLETE_RETENTION_JOB_SQL, FAIL_RETENTION_JOB_SQL, RETENTION_JOB_KEY, RETENTION_RUNNING_STALE_MS, } from "./retention"; export type ScheduledRetentionOutcome = | { status: "skipped"; reason: "already_running" } | { status: "succeeded"; result: RetentionMaintenanceResult }; function scheduledTimestamp(value: number): string { if (!Number.isFinite(value)) { throw new TypeError("scheduled retention requires a finite timestamp"); } const timestamp = new Date(value); if (!Number.isFinite(timestamp.getTime())) { throw new TypeError("scheduled retention requires a valid timestamp"); } return timestamp.toISOString(); } function safeErrorCode(error: unknown): string { if (error instanceof DomainError && /^[a-z0-9_]{3,64}$/.test(error.code)) { return error.code; } return "retention_maintenance_failed"; } export async function runScheduledRetentionMaintenance( db: D1Database, scheduledTime: number, ): Promise { await ensureDatabase(); const scheduledAt = scheduledTimestamp(scheduledTime); const startedAt = new Date().toISOString(); const staleBefore = new Date( new Date(startedAt).getTime() - RETENTION_RUNNING_STALE_MS, ).toISOString(); const runId = `maintenance_${crypto.randomUUID().replaceAll("-", "")}`; const acquired = await db .prepare(ACQUIRE_RETENTION_JOB_SQL) .bind( RETENTION_JOB_KEY, runId, "scheduled", scheduledAt, startedAt, startedAt, startedAt, staleBefore, ) .run(); if (Number(acquired.meta.changes ?? 0) !== 1) { return { status: "skipped", reason: "already_running" }; } try { const result = await runRetentionMaintenance({ actorId: "system:scheduled-retention", confirmed: true, reason: "每日自动清理已经到期的技术记录", now: startedAt, }); const completedAt = new Date().toISOString(); const completion = await db .prepare(COMPLETE_RETENTION_JOB_SQL) .bind( completedAt, completedAt, JSON.stringify(result), completedAt, RETENTION_JOB_KEY, runId, ) .run(); if (Number(completion.meta.changes ?? 0) !== 1) { throw new DomainError( "retention_run_fenced", "到期数据清理完成,但运行状态已经被更新的任务接管", 409, ); } return { status: "succeeded", result }; } catch (error) { const failedAt = new Date().toISOString(); await db .prepare(FAIL_RETENTION_JOB_SQL) .bind( failedAt, safeErrorCode(error), failedAt, RETENTION_JOB_KEY, runId, ) .run(); throw error; } }