Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions dashboard/src/components/document-sandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isDocumentJobActive,
DocumentUploadError,
parseDocumentSandboxSession,
resolveDocumentSandboxSessionRestore,
SANDBOX_FORBIDDEN_MESSAGE,
serializeDocumentSandboxSession,
type DocumentJobResult,
Expand Down Expand Up @@ -79,7 +80,14 @@ export function DocumentSandbox() {
}

useEffect(() => {
const session = parseDocumentSandboxSession(window.sessionStorage.getItem(SANDBOX_SESSION_STORAGE_KEY));
let session: ReturnType<typeof parseDocumentSandboxSession>;
try {
session = parseDocumentSandboxSession(window.sessionStorage.getItem(SANDBOX_SESSION_STORAGE_KEY));
} catch {
setMessage("This browser cannot access the previous sandbox OCR session.");
setRestoring(false);
return;
}
if (!session) {
setRestoring(false);
return;
Expand All @@ -89,24 +97,35 @@ export function DocumentSandbox() {
setMessage("Restoring the last sandbox OCR session…");
void (async () => {
try {
const [restoredUpload, restoredJob] = await Promise.all([
const [uploadResult, jobResult] = await Promise.allSettled([
session.upload_id ? getSandboxDocumentStatus(session.upload_id) : Promise.resolve(null),
session.job_id ? getSandboxDocumentJobStatus(session.job_id) : Promise.resolve(null),
]);
if (cancelled) return;
const restored = resolveDocumentSandboxSessionRestore(uploadResult, jobResult);
const { upload: restoredUpload, job: restoredJob } = restored;
if (restoredUpload) setUpload(restoredUpload);
if (restored.uploadUnavailable) recordActivity("The previous sandbox upload is no longer available.");
if (restoredJob) {
setJob(restoredJob);
recordActivity(`OCR job status restored: ${restoredJob.status}.`);
if (RESULT_READY_JOB_STATUSES.has(restoredJob.status)) {
try {
setResult(await getSandboxDocumentJobResult(restoredJob.job_id));
const restoredResult = await getSandboxDocumentJobResult(restoredJob.job_id);
if (!cancelled) setResult(restoredResult);
} catch (error) {
if (!(error instanceof DocumentResultNotReadyError)) throw error;
}
}
}
if (!cancelled) setMessage("The last sandbox OCR session was restored.");
if (restored.jobUnavailable) recordActivity("The previous sandbox OCR job is no longer available.");
if (!cancelled) {
setMessage(
restoredUpload || restoredJob
? "The last sandbox OCR session was restored."
: "The previous sandbox OCR session is no longer available.",
);
}
} catch (error) {
if (!cancelled) {
setMessage(describe(error, "The previous sandbox OCR session could not be restored. Try refreshing again shortly."));
Expand Down
16 changes: 16 additions & 0 deletions dashboard/src/lib/document-intelligence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
parseDocumentJobState,
parseDocumentSandboxSession,
parseDocumentUploadState,
resolveDocumentSandboxSessionRestore,
} from "./document-intelligence.ts";

test("restores only opaque sandbox identifiers from a browser session", () => {
Expand All @@ -26,6 +27,21 @@ test("rejects browser session data that includes document content or invalid ide
assert.equal(parseDocumentSandboxSession("not json"), null);
});

test("keeps an authorized OCR job when its upload record can no longer be restored", () => {
assert.deepEqual(
resolveDocumentSandboxSessionRestore(
{ status: "rejected", reason: new Error("upload expired") },
{ status: "fulfilled", value: { job_id: "job_01TEST", status: "completed" } },
),
{
upload: null,
job: { job_id: "job_01TEST", status: "completed" },
uploadUnavailable: true,
jobUnavailable: false,
},
);
});

test("allows cancellation only while an OCR job is non-terminal", () => {
assert.equal(canCancelDocumentJob("accepted"), true);
assert.equal(canCancelDocumentJob("processing"), true);
Expand Down
19 changes: 19 additions & 0 deletions dashboard/src/lib/document-intelligence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export type DocumentSandboxSession = {
job_id?: string;
};

export type DocumentSandboxSessionRestore = {
upload: DocumentUploadState | null;
job: DocumentJobState | null;
uploadUnavailable: boolean;
jobUnavailable: boolean;
};

export type DocumentJobProgress = {
stage: "queued" | "preparing" | "extracting" | "validating" | "complete" | "attention" | "cancelled";
message: string;
Expand Down Expand Up @@ -159,6 +166,18 @@ export function parseDocumentSandboxSession(value: string | null): DocumentSandb
}
}

export function resolveDocumentSandboxSessionRestore(
upload: PromiseSettledResult<DocumentUploadState | null>,
job: PromiseSettledResult<DocumentJobState | null>,
): DocumentSandboxSessionRestore {
return {
upload: upload.status === "fulfilled" ? upload.value : null,
job: job.status === "fulfilled" ? job.value : null,
uploadUnavailable: upload.status === "rejected",
jobUnavailable: job.status === "rejected",
};
}

export function serializeDocumentSandboxSession(session: DocumentSandboxSession): string {
return JSON.stringify(session);
}
Expand Down
Loading