openreader/compute-worker/src/orphan-recovery.ts
Richard R a56aaa2be9 Refactor PDF parsing and rendering logic; migrate to compute-worker protocol
- Removed deprecated functions related to document parsing and blob storage in blobstore.ts.
- Introduced new PDF rendering logic in pdf-preview-renderer.ts and pdf-preview-pdfjs-runtime.ts.
- Updated previews-render.ts to utilize the new PDF rendering functions.
- Refactored user-whisper-align-job.ts to use the compute-worker client for alignment requests.
- Enhanced artifact.ts and operation.ts to validate parsed PDF artifacts and resolve current PDF parses.
- Updated snapshot.ts to align with new worker operation types.
- Adjusted runtime-config.ts to check for compute-worker availability.
- Modified types in parsed-pdf.ts and tts.ts to reflect changes in the compute-worker protocol.
- Added unit tests for PDF artifact validation and compute-worker client contract.
- Removed obsolete pdf-op-key.vitest.spec.ts test file.
2026-06-12 13:43:33 -06:00

100 lines
3.2 KiB
TypeScript

import type {
PdfLayoutJobResult,
WhisperAlignJobResult,
WorkerJobTiming,
WorkerJobState,
WorkerOperationState,
} from './compute/api-contracts';
export type StreamedOperationState = WorkerOperationState<WhisperAlignJobResult | PdfLayoutJobResult>;
export interface OrphanRecoveryStateStore {
getOpStateRecord(opId: string): Promise<{ state: StreamedOperationState; revision: number } | null>;
listOpStates(): Promise<StreamedOperationState[]>;
}
export interface OrphanRecoveryOrchestrator {
markFailedIfUnchanged(input: {
current: StreamedOperationState;
expectedRevision: number;
error: { message: string; code?: string } | string;
updatedAt?: number;
timing?: WorkerJobTiming;
}): Promise<StreamedOperationState | null>;
}
export interface RecoverOrphanedOperationsInput {
operationStateStore: OrphanRecoveryStateStore;
orchestrator: OrphanRecoveryOrchestrator;
whisperTimeoutMs: number;
pdfTimeoutMs: number;
opStaleMs: number;
nowMs?: number;
}
function isInflightStatus(status: WorkerJobState): boolean {
return status === 'queued' || status === 'running';
}
export function getOrphanRecoveryThresholdMs(input: {
state: StreamedOperationState;
whisperTimeoutMs: number;
pdfTimeoutMs: number;
opStaleMs: number;
}): number | null {
if (!isInflightStatus(input.state.status)) return null;
if (input.state.status === 'running') {
return input.state.kind === 'whisper_align' ? input.whisperTimeoutMs : input.pdfTimeoutMs;
}
if (input.state.kind !== 'pdf_layout') return null;
return input.opStaleMs;
}
export async function recoverOrphanedOperations(
input: RecoverOrphanedOperationsInput,
): Promise<Array<Pick<StreamedOperationState, 'opId' | 'kind' | 'status'>>> {
const nowMs = input.nowMs ?? Date.now();
const states = await input.operationStateStore.listOpStates();
const candidateStates = states.filter((state) => (
getOrphanRecoveryThresholdMs({
state,
whisperTimeoutMs: input.whisperTimeoutMs,
pdfTimeoutMs: input.pdfTimeoutMs,
opStaleMs: input.opStaleMs,
}) !== null
));
const recoveredStates: Array<Pick<StreamedOperationState, 'opId' | 'kind' | 'status'>> = [];
for (const candidate of candidateStates) {
const record = await input.operationStateStore.getOpStateRecord(candidate.opId);
if (!record) continue;
const staleAfterMs = getOrphanRecoveryThresholdMs({
state: record.state,
whisperTimeoutMs: input.whisperTimeoutMs,
pdfTimeoutMs: input.pdfTimeoutMs,
opStaleMs: input.opStaleMs,
});
if (staleAfterMs === null) continue;
const ageMs = nowMs - record.state.updatedAt;
if (ageMs <= staleAfterMs) continue;
const recovered = await input.orchestrator.markFailedIfUnchanged({
current: record.state,
expectedRevision: record.revision,
error: {
code: 'WORKER_ORPHANED_OP',
message: `Worker stopped before completion; stale operation recovered during reconciliation after ${staleAfterMs}ms`,
},
updatedAt: nowMs,
});
if (!recovered) continue;
recoveredStates.push({
opId: recovered.opId,
kind: recovered.kind,
status: record.state.status,
});
}
return recoveredStates;
}