Compare commits
No commits in common. "main" and "codex/download-conversion-progress-20261111" have entirely different histories.
main
...
codex/down
@ -4,7 +4,6 @@ import { ThemeProvider } from "@/components/theme-provider";
|
|||||||
import { FaviconUpdater } from "@/components/favicon-updater";
|
import { FaviconUpdater } from "@/components/favicon-updater";
|
||||||
import { ImageDeliveryOptimizer } from "@/components/image-delivery-optimizer";
|
import { ImageDeliveryOptimizer } from "@/components/image-delivery-optimizer";
|
||||||
import { AppUpdateNotice } from "@/components/app-update-notice";
|
import { AppUpdateNotice } from "@/components/app-update-notice";
|
||||||
import { PAGE_DOWNLOAD_ACTIVE_ATTRIBUTE } from "@/lib/client/page-refresh";
|
|
||||||
import { getSiteSettings } from "@/lib/db/queries";
|
import { getSiteSettings } from "@/lib/db/queries";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
@ -143,7 +142,7 @@ const CHUNK_ERROR_RECOVERY_SCRIPT = `\
|
|||||||
showManualRecoveryNotice();
|
showManualRecoveryNotice();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lastReloadAt || document.documentElement.hasAttribute('${PAGE_DOWNLOAD_ACTIVE_ATTRIBUTE}')) return;
|
if (lastReloadAt) return;
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
// If storage is blocked, a reload cannot remember its attempt and must not run.
|
// If storage is blocked, a reload cannot remember its attempt and must not run.
|
||||||
if (!safeSetSessionStorage(RECOVERY_KEY, String(now))) return;
|
if (!safeSetSessionStorage(RECOVERY_KEY, String(now))) return;
|
||||||
@ -154,10 +153,6 @@ const CHUNK_ERROR_RECOVERY_SCRIPT = `\
|
|||||||
showManualRecoveryNotice();
|
showManualRecoveryNotice();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (document.documentElement.hasAttribute('${PAGE_DOWNLOAD_ACTIVE_ATTRIBUTE}')) {
|
|
||||||
if (safeSetSessionStorage(RECOVERY_KEY, '0')) lastReloadAt = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 800);
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter } from "@/components/ui/alert-dialog";
|
||||||
import { isPageDownloadActive, preparePageRefresh, subscribeToPageDownloadActivity } from "@/lib/client/page-refresh";
|
import { preparePageRefresh } from "@/lib/client/page-refresh";
|
||||||
|
|
||||||
export function AppUpdateNotice() {
|
export function AppUpdateNotice() {
|
||||||
const [availableVersion, setAvailableVersion] = useState("");
|
const [availableVersion, setAvailableVersion] = useState("");
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const downloading = useSyncExternalStore(subscribeToPageDownloadActivity, isPageDownloadActive, () => false);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let controller: AbortController | undefined;
|
let controller: AbortController | undefined;
|
||||||
@ -61,7 +60,7 @@ export function AppUpdateNotice() {
|
|||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return <AlertDialog open={Boolean(availableVersion) && !downloading}>
|
return <AlertDialog open={Boolean(availableVersion)}>
|
||||||
<AlertDialogContent className="z-[10001]" overlayClassName="z-[10000]" onEscapeKeyDown={event => event.preventDefault()}>
|
<AlertDialogContent className="z-[10001]" overlayClassName="z-[10000]" onEscapeKeyDown={event => event.preventDefault()}>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>平台已更新</AlertDialogTitle>
|
<AlertDialogTitle>平台已更新</AlertDialogTitle>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { buildImageProxyUrl, getImageDownloadExtension } from "@/lib/image-delivery";
|
import { buildImageProxyUrl, getImageDownloadExtension } from "@/lib/image-delivery";
|
||||||
import { inferOriginalImageUrl } from "@/lib/image-url";
|
import { inferOriginalImageUrl } from "@/lib/image-url";
|
||||||
import { formatDownloadFailure, getDownloadFailure, type DownloadFailure } from "@/lib/canvas/download-error";
|
import { formatDownloadFailure, getDownloadFailure, type DownloadFailure } from "@/lib/canvas/download-error";
|
||||||
import { withActivePageDownload } from "@/lib/client/page-refresh";
|
|
||||||
|
|
||||||
export type CanvasImageDownloadPhase = "preparing" | "converting" | "downloading" | "saving";
|
export type CanvasImageDownloadPhase = "preparing" | "converting" | "downloading" | "saving";
|
||||||
|
|
||||||
@ -193,11 +192,7 @@ function triggerCdnDownload(result: { downloadUrl: string; filename: string; byt
|
|||||||
const PHOTOSHOP_DOWNLOAD_EXTENSIONS = new Set(["jpg", "png", "gif", "bmp", "tiff"]);
|
const PHOTOSHOP_DOWNLOAD_EXTENSIONS = new Set(["jpg", "png", "gif", "bmp", "tiff"]);
|
||||||
|
|
||||||
/** Preserve compatible originals and prepare a PNG for other stored images. */
|
/** Preserve compatible originals and prepare a PNG for other stored images. */
|
||||||
export function downloadOriginalImage(params: DownloadCanvasImageParams): Promise<CanvasImageDownloadResult> {
|
export async function downloadOriginalImage(params: DownloadCanvasImageParams): Promise<CanvasImageDownloadResult> {
|
||||||
return withActivePageDownload(() => downloadOriginalImageFile(params));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadOriginalImageFile(params: DownloadCanvasImageParams): Promise<CanvasImageDownloadResult> {
|
|
||||||
const { url, name, signal, onProgress } = params;
|
const { url, name, signal, onProgress } = params;
|
||||||
const source = String(url || "").trim();
|
const source = String(url || "").trim();
|
||||||
if (!source) throw new Error("图片地址无效");
|
if (!source) throw new Error("图片地址无效");
|
||||||
@ -229,7 +224,7 @@ async function downloadOriginalImageFile(params: DownloadCanvasImageParams): Pro
|
|||||||
// The resolver names the attachment using the stored MIME type.
|
// The resolver names the attachment using the stored MIME type.
|
||||||
const extension = delivery.filename.split(".").pop();
|
const extension = delivery.filename.split(".").pop();
|
||||||
if (!PHOTOSHOP_DOWNLOAD_EXTENSIONS.has(extension)) {
|
if (!PHOTOSHOP_DOWNLOAD_EXTENSIONS.has(extension)) {
|
||||||
return downloadPngFile(params, "converting");
|
return downloadCanvasImageAsPng(params, "converting");
|
||||||
}
|
}
|
||||||
const result = triggerCdnDownload(delivery, signal);
|
const result = triggerCdnDownload(delivery, signal);
|
||||||
onProgress?.({ phase: "downloading", message: "已开始下载,可在浏览器下载列表查看进度" });
|
onProgress?.({ phase: "downloading", message: "已开始下载,可在浏览器下载列表查看进度" });
|
||||||
@ -244,7 +239,7 @@ async function downloadOriginalImageFile(params: DownloadCanvasImageParams): Pro
|
|||||||
// A local blob can still be downloaded while its upload is in progress.
|
// A local blob can still be downloaded while its upload is in progress.
|
||||||
if (/^https?:$/.test(target.protocol) && !PHOTOSHOP_DOWNLOAD_EXTENSIONS.has(extension)) {
|
if (/^https?:$/.test(target.protocol) && !PHOTOSHOP_DOWNLOAD_EXTENSIONS.has(extension)) {
|
||||||
void response.body?.cancel().catch(() => {});
|
void response.body?.cancel().catch(() => {});
|
||||||
return downloadPngFile(params, "converting");
|
return downloadCanvasImageAsPng(params, "converting");
|
||||||
}
|
}
|
||||||
const blob = await readResponseBlobWithProgress(response, onProgress);
|
const blob = await readResponseBlobWithProgress(response, onProgress);
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
@ -312,17 +307,13 @@ async function readResponseBlobWithProgress(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadCanvasImageAsPng(params: DownloadCanvasImageParams): Promise<CanvasImageDownloadResult> {
|
export async function downloadCanvasImageAsPng({
|
||||||
return withActivePageDownload(() => downloadPngFile(params, "preparing"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadPngFile({
|
|
||||||
candidates,
|
candidates,
|
||||||
name,
|
name,
|
||||||
onProgress,
|
onProgress,
|
||||||
signal,
|
signal,
|
||||||
url,
|
url,
|
||||||
}: DownloadCanvasImageParams, preparationPhase: "preparing" | "converting"): Promise<CanvasImageDownloadResult> {
|
}: DownloadCanvasImageParams, preparationPhase: "preparing" | "converting" = "preparing"): Promise<CanvasImageDownloadResult> {
|
||||||
const raw = String(url || "").trim();
|
const raw = String(url || "").trim();
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
throw new Error("图片地址无效");
|
throw new Error("图片地址无效");
|
||||||
|
|||||||
@ -1,44 +1,10 @@
|
|||||||
export const BEFORE_PAGE_REFRESH = "app:before-page-refresh";
|
export const BEFORE_PAGE_REFRESH = "app:before-page-refresh";
|
||||||
export const PAGE_DOWNLOAD_ACTIVE_ATTRIBUTE = "data-page-download-active";
|
|
||||||
export type PageRefreshRequest = {
|
export type PageRefreshRequest = {
|
||||||
save: (action: () => Promise<void>) => void;
|
save: (action: () => Promise<void>) => void;
|
||||||
block: (message: string) => void;
|
block: (message: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadListeners = new Set<() => void>();
|
|
||||||
let activePageDownloads = 0;
|
|
||||||
|
|
||||||
export function isPageDownloadActive() {
|
|
||||||
return activePageDownloads > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function subscribeToPageDownloadActivity(listener: () => void) {
|
|
||||||
downloadListeners.add(listener);
|
|
||||||
return () => { downloadListeners.delete(listener); };
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishDownloadActivity() {
|
|
||||||
if (typeof document !== "undefined") {
|
|
||||||
document.documentElement?.toggleAttribute(PAGE_DOWNLOAD_ACTIVE_ATTRIBUTE, isPageDownloadActive());
|
|
||||||
}
|
|
||||||
for (const listener of downloadListeners) listener();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function withActivePageDownload<T>(download: () => Promise<T>): Promise<T> {
|
|
||||||
if (++activePageDownloads === 1) publishDownloadActivity();
|
|
||||||
try {
|
|
||||||
return await download();
|
|
||||||
} finally {
|
|
||||||
if (--activePageDownloads === 0) publishDownloadActivity();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertPageDownloadFinished() {
|
|
||||||
if (isPageDownloadActive()) throw new Error("文件正在下载,请等下载完成后再刷新。");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function preparePageRefresh() {
|
export async function preparePageRefresh() {
|
||||||
assertPageDownloadFinished();
|
|
||||||
const actions: Array<() => Promise<void>> = [];
|
const actions: Array<() => Promise<void>> = [];
|
||||||
let blocked = "";
|
let blocked = "";
|
||||||
window.dispatchEvent(new CustomEvent<PageRefreshRequest>(BEFORE_PAGE_REFRESH, {
|
window.dispatchEvent(new CustomEvent<PageRefreshRequest>(BEFORE_PAGE_REFRESH, {
|
||||||
@ -46,5 +12,4 @@ export async function preparePageRefresh() {
|
|||||||
}));
|
}));
|
||||||
if (blocked) throw new Error(blocked);
|
if (blocked) throw new Error(blocked);
|
||||||
for (const action of actions) await action();
|
for (const action of actions) await action();
|
||||||
assertPageDownloadFinished();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,14 +26,14 @@ function getBatchSize() {
|
|||||||
return Math.max(1, Math.min(50, Math.round(parsed)));
|
return Math.max(1, Math.min(50, Math.round(parsed)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runImageArchiveJob({ preferredTaskId }: { preferredTaskId?: string } = {}) {
|
export async function runImageArchiveJob() {
|
||||||
return trackRuntimeWork(async () => {
|
return trackRuntimeWork(async () => {
|
||||||
const batchSize = getBatchSize();
|
const batchSize = getBatchSize();
|
||||||
console.log(`[ImageArchiveJob] Start image archive job (batchSize=${batchSize})`);
|
console.log(`[ImageArchiveJob] Start image archive job (batchSize=${batchSize})`);
|
||||||
|
|
||||||
await recoverStaleImageArchiveTasks();
|
await recoverStaleImageArchiveTasks();
|
||||||
scheduleImageDownloadPreparation();
|
scheduleImageDownloadPreparation();
|
||||||
const storeTasks = await getImageArchiveTasksToStore({ limit: batchSize, preferredTaskId });
|
const storeTasks = await getImageArchiveTasksToStore({ limit: batchSize });
|
||||||
let storeSucceeded = 0;
|
let storeSucceeded = 0;
|
||||||
let storeFailed = 0;
|
let storeFailed = 0;
|
||||||
|
|
||||||
|
|||||||
@ -98,11 +98,8 @@ export async function enqueueImageArchiveTasks(params: {
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getImageArchiveTasksToStore(params: { limit: number; preferredTaskId?: string }) {
|
export async function getImageArchiveTasksToStore(params: { limit: number }) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const priority = params.preferredTaskId
|
|
||||||
? [asc(sql`case when ${imageArchiveTask.id} = ${params.preferredTaskId} then 0 else 1 end`)]
|
|
||||||
: [];
|
|
||||||
return await db
|
return await db
|
||||||
.select()
|
.select()
|
||||||
.from(imageArchiveTask)
|
.from(imageArchiveTask)
|
||||||
@ -113,7 +110,7 @@ export async function getImageArchiveTasksToStore(params: { limit: number; prefe
|
|||||||
lt(imageArchiveTask.attemptCount, MAX_IMAGE_ARCHIVE_ATTEMPTS),
|
lt(imageArchiveTask.attemptCount, MAX_IMAGE_ARCHIVE_ATTEMPTS),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.orderBy(...priority, asc(imageArchiveTask.createdAt), asc(imageArchiveTask.id))
|
.orderBy(asc(imageArchiveTask.createdAt), asc(imageArchiveTask.id))
|
||||||
.limit(Math.max(1, Math.min(50, params.limit)));
|
.limit(Math.max(1, Math.min(50, params.limit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ type Worker = {
|
|||||||
running: boolean;
|
running: boolean;
|
||||||
scheduled: boolean;
|
scheduled: boolean;
|
||||||
requested: boolean;
|
requested: boolean;
|
||||||
preferredTaskId?: string;
|
|
||||||
retryTimer?: ReturnType<typeof setTimeout>;
|
retryTimer?: ReturnType<typeof setTimeout>;
|
||||||
};
|
};
|
||||||
function getWorker(): Worker {
|
function getWorker(): Worker {
|
||||||
@ -14,9 +13,8 @@ function getWorker(): Worker {
|
|||||||
return (root[workerKey] ??= { running: false, scheduled: false, requested: false });
|
return (root[workerKey] ??= { running: false, scheduled: false, requested: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scheduleImageArchiveWorker(deliveryConfirmed = false, preferredTaskId?: string) {
|
export function scheduleImageArchiveWorker(deliveryConfirmed = false) {
|
||||||
const worker = getWorker();
|
const worker = getWorker();
|
||||||
worker.preferredTaskId ??= preferredTaskId;
|
|
||||||
worker.requested = true;
|
worker.requested = true;
|
||||||
if (worker.running || (worker.scheduled && !deliveryConfirmed)) return;
|
if (worker.running || (worker.scheduled && !deliveryConfirmed)) return;
|
||||||
worker.scheduled = true;
|
worker.scheduled = true;
|
||||||
@ -33,9 +31,7 @@ export function scheduleImageArchiveWorker(deliveryConfirmed = false, preferredT
|
|||||||
const { runImageArchiveJob } = await import("@/lib/jobs/image-archive-job");
|
const { runImageArchiveJob } = await import("@/lib/jobs/image-archive-job");
|
||||||
do {
|
do {
|
||||||
worker.requested = false;
|
worker.requested = false;
|
||||||
const nextTaskId = worker.preferredTaskId;
|
const result = await runImageArchiveJob();
|
||||||
worker.preferredTaskId = undefined;
|
|
||||||
const result = await runImageArchiveJob({ preferredTaskId: nextTaskId });
|
|
||||||
retryNeeded = result.stats.pendingStore > 0 || result.stats.pendingReplace > 0 || result.pendingDownloads === true;
|
retryNeeded = result.stats.pendingStore > 0 || result.stats.pendingReplace > 0 || result.pendingDownloads === true;
|
||||||
// Stop when only backoff tasks remain; do not spin while storage is down.
|
// Stop when only backoff tasks remain; do not spin while storage is down.
|
||||||
if (!worker.requested && result.store.total === 0 && result.replace.total === 0 && result.downloadTasks === 0) break;
|
if (!worker.requested && result.store.total === 0 && result.replace.total === 0 && result.downloadTasks === 0) break;
|
||||||
|
|||||||
@ -15,10 +15,6 @@ export async function resolveOriginalCdnDownload(source: string, name: string, s
|
|||||||
let target = new URL(source);
|
let target = new URL(source);
|
||||||
if (target.origin !== base.origin && (!origin || target.origin !== new URL(origin).origin)) {
|
if (target.origin !== base.origin && (!origin || target.origin !== new URL(origin).origin)) {
|
||||||
const task = await findImageArchiveDownloadTask([...new Set([source, inferOriginalImageUrl(source)])]);
|
const task = await findImageArchiveDownloadTask([...new Set([source, inferOriginalImageUrl(source)])]);
|
||||||
if (task?.status === "pending" && !task.targetUrl) {
|
|
||||||
const { scheduleImageArchiveWorker } = await import("./image-archive-worker");
|
|
||||||
scheduleImageArchiveWorker(false, task.id);
|
|
||||||
}
|
|
||||||
target = new URL(task?.targetUrl || source);
|
target = new URL(task?.targetUrl || source);
|
||||||
}
|
}
|
||||||
if (origin && target.origin === new URL(origin).origin && !target.searchParams.has("token")) {
|
if (origin && target.origin === new URL(origin).origin && !target.searchParams.has("token")) {
|
||||||
|
|||||||
@ -242,7 +242,7 @@ test("inline outputs have short, stable preview identities and durable queue pay
|
|||||||
|
|
||||||
test("background storage starts after delivery and coalesces repeated wakeups", async () => {
|
test("background storage starts after delivery and coalesces repeated wakeups", async () => {
|
||||||
const callbacks: Array<() => Promise<void>> = [];
|
const callbacks: Array<() => Promise<void>> = [];
|
||||||
const preferences: Array<string | undefined> = [];
|
let runs = 0;
|
||||||
const module = serverLoader({
|
const module = serverLoader({
|
||||||
"next/server": {
|
"next/server": {
|
||||||
after: (fn: () => Promise<void>) => {
|
after: (fn: () => Promise<void>) => {
|
||||||
@ -250,10 +250,10 @@ test("background storage starts after delivery and coalesces repeated wakeups",
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"@/lib/jobs/image-archive-job": {
|
"@/lib/jobs/image-archive-job": {
|
||||||
runImageArchiveJob: async ({ preferredTaskId }: { preferredTaskId?: string }) => {
|
runImageArchiveJob: async () => {
|
||||||
preferences.push(preferredTaskId);
|
runs += 1;
|
||||||
return {
|
return {
|
||||||
store: { total: preferences.length === 1 ? 1 : 0 },
|
store: { total: runs === 1 ? 1 : 0 },
|
||||||
replace: { total: 0 },
|
replace: { total: 0 },
|
||||||
stats: { pendingStore: 0, pendingReplace: 0 },
|
stats: { pendingStore: 0, pendingReplace: 0 },
|
||||||
downloadTasks: 0,
|
downloadTasks: 0,
|
||||||
@ -262,11 +262,11 @@ test("background storage starts after delivery and coalesces repeated wakeups",
|
|||||||
},
|
},
|
||||||
})("lib/storage/image-archive-worker.ts");
|
})("lib/storage/image-archive-worker.ts");
|
||||||
module.scheduleImageArchiveWorker();
|
module.scheduleImageArchiveWorker();
|
||||||
module.scheduleImageArchiveWorker(false, "requested-image");
|
module.scheduleImageArchiveWorker();
|
||||||
expect(callbacks).toHaveLength(1);
|
expect(callbacks).toHaveLength(1);
|
||||||
expect(preferences).toEqual([]);
|
expect(runs).toBe(0);
|
||||||
await callbacks[0]();
|
await callbacks[0]();
|
||||||
expect(preferences).toEqual(["requested-image", undefined]);
|
expect(runs).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("bucket failure retains the provider URL and retry task", async () => {
|
test("bucket failure retains the provider URL and retry task", async () => {
|
||||||
|
|||||||
@ -54,18 +54,6 @@ test("temporary third-party URLs are never checked with HEAD when no archive is
|
|||||||
expect(await module.resolveOriginalCdnDownload("https://upstream.example/image.png", "image", new AbortController().signal)).toBeNull();
|
expect(await module.resolveOriginalCdnDownload("https://upstream.example/image.png", "image", new AbortController().signal)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("downloading a pending original prioritizes its existing archive task and keeps the proxy available", async () => {
|
|
||||||
const preferred: string[] = [];
|
|
||||||
const module = serverLoader({
|
|
||||||
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
|
|
||||||
"./image-archive-db": { findImageArchiveDownloadTask: async () => ({ id: "pending-image", status: "pending", targetUrl: null }) },
|
|
||||||
"./image-archive-worker": { scheduleImageArchiveWorker: (_delivery: boolean, id: string) => preferred.push(id) },
|
|
||||||
fetch() { throw new Error("The original must remain available through the proxy while it is queued"); },
|
|
||||||
})("lib/storage/original-image-download.ts");
|
|
||||||
expect(await module.resolveOriginalCdnDownload("https://upstream.example/queued.png", "image", new AbortController().signal)).toBeNull();
|
|
||||||
expect(preferred).toEqual(["pending-image"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a CDN metadata timeout falls back to the original Kodo object without reading its body", async () => {
|
test("a CDN metadata timeout falls back to the original Kodo object without reading its body", async () => {
|
||||||
let closed = false;
|
let closed = false;
|
||||||
const module = serverLoader({
|
const module = serverLoader({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user