fix(catalog) : show asset progress and retry transient failures
CI / Verify (push) Successful in 1m32s
CI / Build immutable images and deploy (push) Successful in 1m20s

This commit is contained in:
2026-09-23 18:28:59 +07:00 Unverified
parent ea12c85c21
commit bd84d49108
2 changed files with 12 additions and 8 deletions
+1 -1
View File
@@ -175,7 +175,7 @@ export function StructuredDashboard({
<Card>
<CardContent className="space-y-2 pt-6">
<div className="flex items-center justify-between gap-3 text-sm">
<span>{syncJob.message}</span>
<span className="whitespace-pre-line">{syncJob.message}</span>
<span className="text-muted-foreground">{syncJob.total ? `${syncJob.completed}/${syncJob.total}` : "กำลังเริ่ม…"}</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-muted">
+11 -7
View File
@@ -115,7 +115,7 @@ interface SyncedCharacter {
type SyncControl = { signal?: AbortSignal; checkCancelled: () => Promise<void>; report: (phase: string, completed: number, total: number, message: string) => Promise<void> };
async function retryTimeout<T>(label: string, task: () => Promise<T>, signal?: AbortSignal): Promise<T> {
async function retryTransient<T>(label: string, task: () => Promise<T>, signal?: AbortSignal): Promise<T> {
for (let attempt = 1; ; attempt += 1) {
signal?.throwIfAborted();
try {
@@ -123,7 +123,7 @@ async function retryTimeout<T>(label: string, task: () => Promise<T>, signal?: A
} catch (cause) {
signal?.throwIfAborted();
const message = cause instanceof Error ? cause.message : String(cause);
if (attempt >= 3 || !/timed out|timeout/i.test(message)) {
if (attempt >= 3 || !/timed out|timeout|unexpected error has occurred/i.test(message)) {
throw new Error(`${label}: ${message}`, { cause });
}
await new Promise((resolve) => setTimeout(resolve, attempt * 1_000));
@@ -132,7 +132,7 @@ async function retryTimeout<T>(label: string, task: () => Promise<T>, signal?: A
}
async function getJson(url: string, signal?: AbortSignal): Promise<unknown> {
return retryTimeout(`Lunaris request ${url}`, async () => {
return retryTransient(`Lunaris request ${url}`, async () => {
const response = await fetch(url, {
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
cache: "no-store",
@@ -144,7 +144,7 @@ async function getJson(url: string, signal?: AbortSignal): Promise<unknown> {
}
async function getOptionalJson(url: string, signal?: AbortSignal): Promise<unknown | null> {
return retryTimeout(`Lunaris request ${url}`, async () => {
return retryTransient(`Lunaris request ${url}`, async () => {
const response = await fetch(url, {
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
cache: "no-store",
@@ -229,12 +229,15 @@ async function uploadAssets(
let completed = 0;
let lastProgressAt = 0;
const missing = new Set<string>();
async function reportProgress() {
async function reportProgress(currentAsset?: string) {
if (!control) return;
const now = Date.now();
if (completed < assets.length && now - lastProgressAt < PROGRESS_INTERVAL_MS) return;
lastProgressAt = now;
await control.report("assets", completed, assets.length, `ดาวน์โหลดรูปภาพ ${completed}/${assets.length}`);
const message = currentAsset
? `กำลังดาวน์โหลดรูปภาพ ${Math.min(completed + 1, assets.length)}/${assets.length}\n${currentAsset.split("/").at(-1)}`
: `ดาวน์โหลดรูปภาพ ${completed}/${assets.length}`;
await control.report("assets", completed, assets.length, message);
}
async function worker() {
while (true) {
@@ -242,7 +245,8 @@ async function uploadAssets(
if (index >= assets.length) return;
await control?.checkCancelled();
const asset = assets[index];
await retryTimeout(`Asset ${asset.target}`, async () => {
await reportProgress(asset.target);
await retryTransient(`Asset ${asset.target}`, async () => {
if (!(await storage.file(asset.target).exists())) {
if (asset.body) {
await storage.write(asset.target, asset.body, {