openreader/src/lib/server/jobs/parsePdfJob.ts
Richard R b6be71e3b1 refactor(types,api): centralize PDF and TTS type exports to compute-core/types
Move and re-export all PDF parsing and TTS-related types from
@openreader/compute-core/types, eliminating local type duplication.
Update imports throughout the codebase to use the new centralized type
module. Remove obsolete worker-contract file and update tsconfig paths
and compute-core exports for the new types entry point. Refactor API
routes and job logic to use the new type imports and shared parse-state
healing utility. This streamlines type management and improves
consistency between compute and app layers.
2026-05-22 01:51:51 -06:00

139 lines
4.4 KiB
TypeScript

import { and, eq } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { documentKey, putParsedDocumentBlob } from '@/lib/server/documents/blobstore';
import { stringifyDocumentParseState } from '@/lib/server/documents/parse-state';
import { getCompute } from '@/lib/server/compute';
import { clearTtsSegmentCache } from '@/lib/server/tts/segments-cache';
import type { PdfLayoutProgress } from '@openreader/compute-core/types';
interface ParsePdfJobInput {
documentId: string;
userId: string;
namespace: string | null;
forceToken?: string;
}
const running = new Set<string>();
function keyFor(input: ParsePdfJobInput): string {
return `${input.userId}:${input.documentId}:${input.namespace || ''}`;
}
export async function parsePdfJob(input: ParsePdfJobInput): Promise<void> {
const key = keyFor(input);
if (running.has(key)) return;
running.add(key);
try {
const now = Date.now();
await db
.update(documents)
.set({
parseState: stringifyDocumentParseState({
status: 'running',
progress: null,
updatedAt: now,
}),
})
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
const compute = await getCompute();
const writeProgress = async (progress: PdfLayoutProgress): Promise<void> => {
await db
.update(documents)
.set({
parseState: stringifyDocumentParseState({
status: 'running',
progress: {
totalPages: progress.totalPages,
pagesParsed: progress.pagesParsed,
currentPage: progress.currentPage,
phase: progress.phase,
},
updatedAt: Date.now(),
}),
})
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
};
const layout = await compute.parsePdfLayout({
documentId: input.documentId,
namespace: input.namespace,
documentObjectKey: documentKey(input.documentId, input.namespace),
forceToken: input.forceToken,
onProgress: writeProgress,
});
let parsedJsonKey = layout.parsedObjectKey ?? null;
if (!parsedJsonKey) {
if (!layout.parsed) throw new Error('Compute backend did not return parsed result');
const parsedJson = Buffer.from(JSON.stringify(layout.parsed));
parsedJsonKey = await putParsedDocumentBlob(input.documentId, parsedJson, input.namespace);
}
const cleared = await clearTtsSegmentCache({
userId: input.userId,
documentId: input.documentId,
readerType: 'pdf',
});
if (cleared.warning) {
console.warn('[parsePdfJob] cache invalidation warning', {
documentId: input.documentId,
warning: cleared.warning,
});
}
await db
.update(documents)
.set({
parseState: stringifyDocumentParseState({
status: 'ready',
progress: null,
updatedAt: Date.now(),
}),
parsedJsonKey,
})
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
const cause = error instanceof Error ? error.cause : undefined;
const parseStatus = 'failed';
try {
await db
.update(documents)
.set({
parseState: stringifyDocumentParseState({
status: 'failed',
progress: null,
updatedAt: Date.now(),
error: message,
}),
})
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
} catch (statusError) {
console.error('[parsePdfJob] failed to write parse status', {
documentId: input.documentId,
parseStatus,
error: statusError instanceof Error ? statusError.message : String(statusError),
});
}
console.error('[parsePdfJob] failed', {
documentId: input.documentId,
parseStatus,
error: message,
...(stack ? { stack } : {}),
...(cause ? { cause: String(cause) } : {}),
});
} finally {
running.delete(key);
}
}
export function enqueueParsePdfJob(input: ParsePdfJobInput): void {
Promise.resolve()
.then(() => parsePdfJob(input))
.catch((error) => {
console.error('[parsePdfJob] uncaught error', error);
});
}