pdf parse: dedupe shared jobs and stop enqueue-on-read
This commit is contained in:
parent
12c586d7be
commit
b2517bc91d
4 changed files with 270 additions and 86 deletions
|
|
@ -3,7 +3,6 @@ import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { enqueueParsePdfJob } from '@/lib/server/jobs/user-pdf-layout-job';
|
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import type { PdfParseProgress, PdfParseStatus } from '@/types/parsed-pdf';
|
import type { PdfParseProgress, PdfParseStatus } from '@/types/parsed-pdf';
|
||||||
|
|
@ -96,13 +95,6 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial = await toSnapshot(row);
|
const initial = await toSnapshot(row);
|
||||||
if (initial.parseStatus === 'pending') {
|
|
||||||
enqueueParsePdfJob({
|
|
||||||
documentId: id,
|
|
||||||
userId: row.userId,
|
|
||||||
namespace: testNamespace,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
|
@ -132,13 +124,6 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||||
if (!nextRow) break;
|
if (!nextRow) break;
|
||||||
|
|
||||||
const next = await toSnapshot(nextRow);
|
const next = await toSnapshot(nextRow);
|
||||||
if (next.parseStatus === 'pending') {
|
|
||||||
enqueueParsePdfJob({
|
|
||||||
documentId: id,
|
|
||||||
userId: nextRow.userId,
|
|
||||||
namespace: testNamespace,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSignature = JSON.stringify(next);
|
const nextSignature = JSON.stringify(next);
|
||||||
if (nextSignature !== signature) {
|
if (nextSignature !== signature) {
|
||||||
|
|
|
||||||
|
|
@ -102,13 +102,6 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveStatus !== 'ready') {
|
if (effectiveStatus !== 'ready') {
|
||||||
if (effectiveStatus === 'pending') {
|
|
||||||
enqueueParsePdfJob({
|
|
||||||
documentId: id,
|
|
||||||
userId: row.userId,
|
|
||||||
namespace: testNamespace,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
parseStatus: effectiveStatus,
|
parseStatus: effectiveStatus,
|
||||||
parseProgress: state.progress ?? null,
|
parseProgress: state.progress ?? null,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { safeDocumentName } from '@/lib/server/documents/utils';
|
import { safeDocumentName } from '@/lib/server/documents/utils';
|
||||||
import { enqueueDocumentPreview } from '@/lib/server/documents/previews';
|
import { enqueueDocumentPreview } from '@/lib/server/documents/previews';
|
||||||
|
import { enqueueParsePdfJob } from '@/lib/server/jobs/user-pdf-layout-job';
|
||||||
|
import { stringifyDocumentParseState } from '@/lib/server/documents/parse-state';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { putDocumentBlob } from '@/lib/server/documents/blobstore';
|
import { putDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||||
|
|
@ -136,6 +138,8 @@ export async function POST(req: NextRequest) {
|
||||||
size: pdfContent.length,
|
size: pdfContent.length,
|
||||||
lastModified,
|
lastModified,
|
||||||
filePath: id,
|
filePath: id,
|
||||||
|
parseState: stringifyDocumentParseState({ status: 'pending', progress: null, updatedAt: Date.now() }),
|
||||||
|
parsedJsonKey: null,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: [documents.id, documents.userId],
|
target: [documents.id, documents.userId],
|
||||||
|
|
@ -145,6 +149,8 @@ export async function POST(req: NextRequest) {
|
||||||
size: pdfContent.length,
|
size: pdfContent.length,
|
||||||
lastModified,
|
lastModified,
|
||||||
filePath: id,
|
filePath: id,
|
||||||
|
parseState: stringifyDocumentParseState({ status: 'pending', progress: null, updatedAt: Date.now() }),
|
||||||
|
parsedJsonKey: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -159,6 +165,12 @@ export async function POST(req: NextRequest) {
|
||||||
console.error(`Failed to enqueue preview for converted DOCX ${id}:`, error);
|
console.error(`Failed to enqueue preview for converted DOCX ${id}:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
enqueueParsePdfJob({
|
||||||
|
documentId: id,
|
||||||
|
userId: storageUserId,
|
||||||
|
namespace: testNamespace,
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
stored: {
|
stored: {
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { and, eq } from 'drizzle-orm';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { and, eq, inArray, isNull } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { documentKey, putParsedDocumentBlob } from '@/lib/server/documents/blobstore';
|
import { documentKey, putParsedDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||||
import { stringifyDocumentParseState } from '@/lib/server/documents/parse-state';
|
import { parseDocumentParseState, stringifyDocumentParseState } from '@/lib/server/documents/parse-state';
|
||||||
import { getCompute } from '@/lib/server/compute';
|
import { getCompute } from '@/lib/server/compute';
|
||||||
import { clearTtsSegmentCache } from '@/lib/server/tts/segments-cache';
|
import { clearTtsSegmentCache } from '@/lib/server/tts/segments-cache';
|
||||||
|
import { UNCLAIMED_USER_ID } from '@/lib/server/storage/docstore-legacy';
|
||||||
import type { PdfLayoutJobBase, PdfLayoutProgress } from '@openreader/compute-core/api-contracts';
|
import type { PdfLayoutJobBase, PdfLayoutProgress } from '@openreader/compute-core/api-contracts';
|
||||||
|
|
||||||
type UserPdfLayoutJobRequest = PdfLayoutJobBase & {
|
type UserPdfLayoutJobRequest = PdfLayoutJobBase & {
|
||||||
|
|
@ -14,45 +16,231 @@ type UserPdfLayoutJobRequest = PdfLayoutJobBase & {
|
||||||
|
|
||||||
const running = new Set<string>();
|
const running = new Set<string>();
|
||||||
|
|
||||||
|
const FOLLOWER_WAIT_TIMEOUT_MS = 180_000;
|
||||||
|
const FOLLOWER_POLL_MS = 1_200;
|
||||||
|
|
||||||
|
type ParseRow = {
|
||||||
|
userId: string;
|
||||||
|
parseState: string | null;
|
||||||
|
parsedJsonKey: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
function keyFor(input: UserPdfLayoutJobRequest): string {
|
function keyFor(input: UserPdfLayoutJobRequest): string {
|
||||||
return `${input.userId}:${input.documentId}:${input.namespace || ''}`;
|
const forceToken = input.forceToken?.trim();
|
||||||
|
if (forceToken) {
|
||||||
|
return `force:${input.documentId}:${input.namespace || ''}:${forceToken}`;
|
||||||
|
}
|
||||||
|
return `shared:${input.documentId}:${input.namespace || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferNamespaceFromUnclaimedUserId(userId: string): string | null {
|
||||||
|
const prefix = `${UNCLAIMED_USER_ID}::`;
|
||||||
|
if (!userId.startsWith(prefix)) return null;
|
||||||
|
const ns = userId.slice(prefix.length).trim();
|
||||||
|
return ns || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowMatchesScope(row: ParseRow, input: UserPdfLayoutJobRequest): boolean {
|
||||||
|
const inferredNamespace = inferNamespaceFromUnclaimedUserId(row.userId);
|
||||||
|
if (inferredNamespace !== null) {
|
||||||
|
return inferredNamespace === (input.namespace ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular (non-unclaimed) users are only shared in the non-namespaced path.
|
||||||
|
if (!input.namespace) return true;
|
||||||
|
|
||||||
|
// In namespaced contexts with regular users, avoid touching other users'
|
||||||
|
// rows since namespace is not persisted on document rows.
|
||||||
|
return row.userId === input.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadScopedRows(input: UserPdfLayoutJobRequest): Promise<ParseRow[]> {
|
||||||
|
const rows = (await db
|
||||||
|
.select({
|
||||||
|
userId: documents.userId,
|
||||||
|
parseState: documents.parseState,
|
||||||
|
parsedJsonKey: documents.parsedJsonKey,
|
||||||
|
})
|
||||||
|
.from(documents)
|
||||||
|
.where(eq(documents.id, input.documentId))) as ParseRow[];
|
||||||
|
|
||||||
|
return rows.filter((row) => rowMatchesScope(row, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReadyRow(row: ParseRow): row is ParseRow & { parsedJsonKey: string } {
|
||||||
|
if (!row.parsedJsonKey) return false;
|
||||||
|
return parseDocumentParseState(row.parseState).status === 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
function userIdsFromRows(rows: ParseRow[]): string[] {
|
||||||
|
return Array.from(new Set(rows.map((row) => row.userId).filter(Boolean)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStateMatchCondition(expected: string | null) {
|
||||||
|
return expected === null ? isNull(documents.parseState) : eq(documents.parseState, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateParseStateForUsers(input: {
|
||||||
|
documentId: string;
|
||||||
|
userIds: string[];
|
||||||
|
parseState: string;
|
||||||
|
parsedJsonKey?: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (input.userIds.length === 0) return;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(documents)
|
||||||
|
.set({
|
||||||
|
parseState: input.parseState,
|
||||||
|
...(typeof input.parsedJsonKey === 'string' || input.parsedJsonKey === null
|
||||||
|
? { parsedJsonKey: input.parsedJsonKey }
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(documents.id, input.documentId),
|
||||||
|
input.userIds.length === 1
|
||||||
|
? eq(documents.userId, input.userIds[0])
|
||||||
|
: inArray(documents.userId, input.userIds),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncCallerToSharedResult(input: UserPdfLayoutJobRequest): Promise<void> {
|
||||||
|
const deadline = Date.now() + FOLLOWER_WAIT_TIMEOUT_MS;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const rows = await loadScopedRows(input);
|
||||||
|
const ready = rows.find(isReadyRow);
|
||||||
|
if (ready) {
|
||||||
|
await updateParseStateForUsers({
|
||||||
|
documentId: input.documentId,
|
||||||
|
userIds: [input.userId],
|
||||||
|
parseState: stringifyDocumentParseState({
|
||||||
|
status: 'ready',
|
||||||
|
progress: null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
}),
|
||||||
|
parsedJsonKey: ready.parsedJsonKey,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses = rows.map((row) => parseDocumentParseState(row.parseState));
|
||||||
|
const hasInFlight = statuses.some((state) => state.status === 'pending' || state.status === 'running');
|
||||||
|
const failedState = statuses.find((state) => state.status === 'failed');
|
||||||
|
if (failedState && !hasInFlight) {
|
||||||
|
await updateParseStateForUsers({
|
||||||
|
documentId: input.documentId,
|
||||||
|
userIds: [input.userId],
|
||||||
|
parseState: stringifyDocumentParseState({
|
||||||
|
status: 'failed',
|
||||||
|
progress: null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
...(failedState.error ? { error: failedState.error } : {}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(FOLLOWER_POLL_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void> {
|
export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void> {
|
||||||
const key = keyFor(input);
|
const key = keyFor(input);
|
||||||
if (running.has(key)) return;
|
if (running.has(key)) {
|
||||||
|
if (!input.forceToken?.trim()) {
|
||||||
|
await syncCallerToSharedResult(input);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
running.add(key);
|
running.add(key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const scopedRows = await loadScopedRows(input);
|
||||||
await db
|
const scopedUserIds = userIdsFromRows(scopedRows);
|
||||||
|
|
||||||
|
// Non-force jobs can short-circuit by reusing existing ready output from
|
||||||
|
// any row in the same document scope.
|
||||||
|
if (!input.forceToken?.trim()) {
|
||||||
|
const ready = scopedRows.find(isReadyRow);
|
||||||
|
if (ready) {
|
||||||
|
await updateParseStateForUsers({
|
||||||
|
documentId: input.documentId,
|
||||||
|
userIds: [input.userId],
|
||||||
|
parseState: stringifyDocumentParseState({
|
||||||
|
status: 'ready',
|
||||||
|
progress: null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
}),
|
||||||
|
parsedJsonKey: ready.parsedJsonKey,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const coordinator = [...scopedRows].sort((a, b) => a.userId.localeCompare(b.userId))[0];
|
||||||
|
if (!coordinator) return;
|
||||||
|
|
||||||
|
const claimOpId = randomUUID();
|
||||||
|
const runningState = stringifyDocumentParseState({
|
||||||
|
status: 'running',
|
||||||
|
progress: null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
opId: claimOpId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const claimRows = (await db
|
||||||
.update(documents)
|
.update(documents)
|
||||||
.set({
|
.set({
|
||||||
parseState: stringifyDocumentParseState({
|
parseState: runningState,
|
||||||
status: 'running',
|
|
||||||
progress: null,
|
|
||||||
updatedAt: now,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(documents.id, input.documentId),
|
||||||
|
eq(documents.userId, coordinator.userId),
|
||||||
|
parseStateMatchCondition(coordinator.parseState),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning({ userId: documents.userId })) as Array<{ userId: string }>;
|
||||||
|
|
||||||
|
const claimed = claimRows.some((row) => row.userId === coordinator.userId);
|
||||||
|
if (!claimed) {
|
||||||
|
if (!input.forceToken?.trim()) {
|
||||||
|
await syncCallerToSharedResult(input);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cohortUserIds = Array.from(new Set([...scopedUserIds, input.userId, coordinator.userId]));
|
||||||
|
await updateParseStateForUsers({
|
||||||
|
documentId: input.documentId,
|
||||||
|
userIds: cohortUserIds,
|
||||||
|
parseState: runningState,
|
||||||
|
});
|
||||||
|
|
||||||
const compute = await getCompute();
|
const compute = await getCompute();
|
||||||
const writeProgress = async (progress: PdfLayoutProgress): Promise<void> => {
|
const writeProgress = async (progress: PdfLayoutProgress): Promise<void> => {
|
||||||
await db
|
await updateParseStateForUsers({
|
||||||
.update(documents)
|
documentId: input.documentId,
|
||||||
.set({
|
userIds: cohortUserIds,
|
||||||
parseState: stringifyDocumentParseState({
|
parseState: stringifyDocumentParseState({
|
||||||
status: 'running',
|
status: 'running',
|
||||||
progress: {
|
progress: {
|
||||||
totalPages: progress.totalPages,
|
totalPages: progress.totalPages,
|
||||||
pagesParsed: progress.pagesParsed,
|
pagesParsed: progress.pagesParsed,
|
||||||
currentPage: progress.currentPage,
|
currentPage: progress.currentPage,
|
||||||
phase: progress.phase,
|
phase: progress.phase,
|
||||||
},
|
},
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}),
|
opId: claimOpId,
|
||||||
})
|
}),
|
||||||
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
|
});
|
||||||
};
|
};
|
||||||
const layout = await compute.parsePdfLayout({
|
const layout = await compute.parsePdfLayout({
|
||||||
documentId: input.documentId,
|
documentId: input.documentId,
|
||||||
|
|
@ -69,53 +257,59 @@ export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void>
|
||||||
parsedJsonKey = await putParsedDocumentBlob(input.documentId, parsedJson, input.namespace);
|
parsedJsonKey = await putParsedDocumentBlob(input.documentId, parsedJson, input.namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
const finalScopedRows = await loadScopedRows(input);
|
||||||
.update(documents)
|
const finalUserIds = userIdsFromRows(finalScopedRows);
|
||||||
.set({
|
await updateParseStateForUsers({
|
||||||
parseState: stringifyDocumentParseState({
|
documentId: input.documentId,
|
||||||
status: 'ready',
|
userIds: finalUserIds.length > 0 ? finalUserIds : cohortUserIds,
|
||||||
progress: null,
|
parseState: stringifyDocumentParseState({
|
||||||
updatedAt: Date.now(),
|
status: 'ready',
|
||||||
}),
|
progress: null,
|
||||||
parsedJsonKey,
|
updatedAt: Date.now(),
|
||||||
})
|
}),
|
||||||
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
|
parsedJsonKey,
|
||||||
|
});
|
||||||
|
|
||||||
// Best-effort cache invalidation should not block parse readiness.
|
// Best-effort cache invalidation should not block parse readiness.
|
||||||
void clearTtsSegmentCache({
|
for (const userId of (finalUserIds.length > 0 ? finalUserIds : cohortUserIds)) {
|
||||||
userId: input.userId,
|
void clearTtsSegmentCache({
|
||||||
documentId: input.documentId,
|
userId,
|
||||||
readerType: 'pdf',
|
|
||||||
}).then((cleared) => {
|
|
||||||
if (cleared.warning) {
|
|
||||||
console.warn('[parsePdfJob] cache invalidation warning', {
|
|
||||||
documentId: input.documentId,
|
|
||||||
warning: cleared.warning,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch((cacheError) => {
|
|
||||||
console.warn('[parsePdfJob] cache invalidation failed', {
|
|
||||||
documentId: input.documentId,
|
documentId: input.documentId,
|
||||||
error: cacheError instanceof Error ? cacheError.message : String(cacheError),
|
readerType: 'pdf',
|
||||||
|
}).then((cleared) => {
|
||||||
|
if (cleared.warning) {
|
||||||
|
console.warn('[parsePdfJob] cache invalidation warning', {
|
||||||
|
documentId: input.documentId,
|
||||||
|
userId,
|
||||||
|
warning: cleared.warning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((cacheError) => {
|
||||||
|
console.warn('[parsePdfJob] cache invalidation failed', {
|
||||||
|
documentId: input.documentId,
|
||||||
|
userId,
|
||||||
|
error: cacheError instanceof Error ? cacheError.message : String(cacheError),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
const stack = error instanceof Error ? error.stack : undefined;
|
const stack = error instanceof Error ? error.stack : undefined;
|
||||||
const cause = error instanceof Error ? error.cause : undefined;
|
const cause = error instanceof Error ? error.cause : undefined;
|
||||||
const parseStatus = 'failed';
|
const parseStatus = 'failed';
|
||||||
try {
|
try {
|
||||||
await db
|
const scopedRows = await loadScopedRows(input);
|
||||||
.update(documents)
|
const scopedUserIds = userIdsFromRows(scopedRows);
|
||||||
.set({
|
await updateParseStateForUsers({
|
||||||
parseState: stringifyDocumentParseState({
|
documentId: input.documentId,
|
||||||
status: 'failed',
|
userIds: scopedUserIds.length > 0 ? scopedUserIds : [input.userId],
|
||||||
progress: null,
|
parseState: stringifyDocumentParseState({
|
||||||
updatedAt: Date.now(),
|
status: 'failed',
|
||||||
error: message,
|
progress: null,
|
||||||
}),
|
updatedAt: Date.now(),
|
||||||
})
|
error: message,
|
||||||
.where(and(eq(documents.id, input.documentId), eq(documents.userId, input.userId)));
|
}),
|
||||||
|
});
|
||||||
} catch (statusError) {
|
} catch (statusError) {
|
||||||
console.error('[parsePdfJob] failed to write parse status', {
|
console.error('[parsePdfJob] failed to write parse status', {
|
||||||
documentId: input.documentId,
|
documentId: input.documentId,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue