mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
fix(tools): stop the preview spinner hanging when thumbnail generation stalls
This commit is contained in:
@@ -406,13 +406,19 @@ export const useToolOperation = <TParams>(
|
||||
|
||||
actions.setFiles(processedFiles);
|
||||
|
||||
// Generate thumbnails and download URL concurrently
|
||||
// Generate thumbnails and download URL concurrently. The flag has to clear in a
|
||||
// finally: anything thrown here otherwise leaves the spinner up permanently.
|
||||
actions.setGeneratingThumbnails(true);
|
||||
const [thumbnails, downloadInfo] = await Promise.all([
|
||||
generateThumbnails(processedFiles),
|
||||
createDownloadInfo(processedFiles, config.operationType),
|
||||
]);
|
||||
actions.setGeneratingThumbnails(false);
|
||||
let thumbnails: string[];
|
||||
let downloadInfo: { url: string; filename: string };
|
||||
try {
|
||||
[thumbnails, downloadInfo] = await Promise.all([
|
||||
generateThumbnails(processedFiles),
|
||||
createDownloadInfo(processedFiles, config.operationType),
|
||||
]);
|
||||
} finally {
|
||||
actions.setGeneratingThumbnails(false);
|
||||
}
|
||||
|
||||
actions.setThumbnails(thumbnails);
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { allowConsole, expectConsole } from "@app/tests/failOnConsole";
|
||||
|
||||
vi.mock("@app/utils/thumbnailUtils", () => ({
|
||||
generateThumbnailForFile: vi.fn(),
|
||||
generateThumbnailWithMetadata: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@app/contexts/PreferencesContext", () => ({
|
||||
usePreferences: () => ({
|
||||
preferences: { autoUnzip: false, autoUnzipFileLimit: 10 },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@app/services/zipFileService", () => ({
|
||||
zipFileService: {
|
||||
extractWithPreferences: vi.fn(),
|
||||
createZipFromFiles: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
generateThumbnailForFile,
|
||||
generateThumbnailWithMetadata,
|
||||
} from "@app/utils/thumbnailUtils";
|
||||
import { useToolResources } from "@app/hooks/tools/shared/useToolResources";
|
||||
|
||||
const pdf = (name: string) =>
|
||||
new File(["%PDF-1.4"], name, { type: "application/pdf" });
|
||||
|
||||
describe("useToolResources thumbnail generation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("gives up on a thumbnail that never settles instead of hanging", async () => {
|
||||
expectConsole.warn(/Thumbnail generation timed out for stuck\.pdf/);
|
||||
vi.useFakeTimers();
|
||||
// A wedged pdfium worker never replies, so the promise never settles.
|
||||
vi.mocked(generateThumbnailForFile).mockReturnValue(new Promise(() => {}));
|
||||
|
||||
const { result } = renderHook(() => useToolResources());
|
||||
const pending = result.current.generateThumbnails([pdf("stuck.pdf")]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
|
||||
await expect(pending).resolves.toEqual([""]);
|
||||
});
|
||||
|
||||
test("gives up on metadata generation that never settles", async () => {
|
||||
expectConsole.warn(/Thumbnail generation timed out for stuck\.pdf/);
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(generateThumbnailWithMetadata).mockReturnValue(
|
||||
new Promise(() => {}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useToolResources());
|
||||
const pending = result.current.generateThumbnailsWithMetadata([
|
||||
pdf("stuck.pdf"),
|
||||
]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
|
||||
await expect(pending).resolves.toEqual([{ thumbnail: "", pageCount: 1 }]);
|
||||
});
|
||||
|
||||
test("one stuck file does not block the rest", async () => {
|
||||
expectConsole.warn(/Thumbnail generation timed out for stuck\.pdf/);
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(generateThumbnailForFile)
|
||||
.mockReturnValueOnce(new Promise(() => {}))
|
||||
.mockResolvedValueOnce("data:image/png;base64,ok");
|
||||
|
||||
const { result } = renderHook(() => useToolResources());
|
||||
const pending = result.current.generateThumbnails([
|
||||
pdf("stuck.pdf"),
|
||||
pdf("fine.pdf"),
|
||||
]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
|
||||
await expect(pending).resolves.toEqual(["", "data:image/png;base64,ok"]);
|
||||
});
|
||||
|
||||
test("still returns real thumbnails when generation succeeds", async () => {
|
||||
vi.mocked(generateThumbnailForFile).mockResolvedValue(
|
||||
"data:image/png;base64,ok",
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useToolResources());
|
||||
|
||||
await expect(
|
||||
result.current.generateThumbnails([pdf("fine.pdf")]),
|
||||
).resolves.toEqual(["data:image/png;base64,ok"]);
|
||||
});
|
||||
|
||||
test("a rejected thumbnail still resolves to a placeholder", async () => {
|
||||
allowConsole.warn(/Failed to generate thumbnail/);
|
||||
vi.mocked(generateThumbnailForFile).mockRejectedValue(new Error("boom"));
|
||||
|
||||
const { result } = renderHook(() => useToolResources());
|
||||
|
||||
await expect(
|
||||
result.current.generateThumbnails([pdf("bad.pdf")]),
|
||||
).resolves.toEqual([""]);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,34 @@ import {
|
||||
import { zipFileService } from "@app/services/zipFileService";
|
||||
import { usePreferences } from "@app/contexts/PreferencesContext";
|
||||
|
||||
// A wedged pdfium worker never replies, so awaiting it pinned the review panel on
|
||||
// "Generating previews..." with no way out but a page reload.
|
||||
const THUMBNAIL_TIMEOUT_MS = 30_000;
|
||||
|
||||
function withThumbnailTimeout<T>(
|
||||
work: Promise<T>,
|
||||
fallback: T,
|
||||
fileName: string,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(`Thumbnail generation timed out for ${fileName}`);
|
||||
resolve(fallback);
|
||||
}, THUMBNAIL_TIMEOUT_MS);
|
||||
|
||||
work.then(
|
||||
(value) => {
|
||||
clearTimeout(timer);
|
||||
resolve(value);
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const useToolResources = () => {
|
||||
const { preferences } = usePreferences();
|
||||
const [blobUrls, setBlobUrls] = useState<string[]>([]);
|
||||
@@ -59,7 +87,11 @@ export const useToolResources = () => {
|
||||
console.log(
|
||||
`🖼️ Generating thumbnail for: ${file.name} (${file.type}, ${file.size} bytes)`,
|
||||
);
|
||||
const thumbnail = await generateThumbnailForFile(file);
|
||||
const thumbnail = await withThumbnailTimeout(
|
||||
generateThumbnailForFile(file),
|
||||
"",
|
||||
file.name,
|
||||
);
|
||||
console.log(`🖼️ Generated thumbnail for ${file.name}: SUCCESS`);
|
||||
thumbnails.push(thumbnail);
|
||||
} catch (error) {
|
||||
@@ -88,7 +120,11 @@ export const useToolResources = () => {
|
||||
console.log(
|
||||
`🖼️ Generating thumbnail with metadata for: ${file.name} (${file.type}, ${file.size} bytes)`,
|
||||
);
|
||||
const result = await generateThumbnailWithMetadata(file);
|
||||
const result = await withThumbnailTimeout(
|
||||
generateThumbnailWithMetadata(file),
|
||||
{ thumbnail: "", pageCount: 1 },
|
||||
file.name,
|
||||
);
|
||||
console.log(
|
||||
`🖼️ Generated thumbnail with metadata for ${file.name}: SUCCESS, ${result.pageCount} pages`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user