feat : let discord fetcher can stop working and accept every message type
CI / Verify (push) Successful in 1m48s
CI / Build immutable images and deploy (push) Successful in 3m21s

This commit is contained in:
2026-09-21 18:29:17 +07:00 Unverified
parent 481123eec0
commit 88afa8e947
4 changed files with 132 additions and 115 deletions
+63 -37
View File
@@ -113,27 +113,47 @@ interface SyncedCharacter {
baseStats: CatalogBaseStat[];
}
type SyncControl = { checkCancelled: () => Promise<void>; report: (phase: string, completed: number, total: number, message: string) => Promise<void> };
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> {
for (let attempt = 1; ; attempt += 1) {
signal?.throwIfAborted();
try {
return await task();
} catch (cause) {
signal?.throwIfAborted();
const message = cause instanceof Error ? cause.message : String(cause);
if (attempt >= 3 || !/timed out|timeout/i.test(message)) {
throw new Error(`${label}: ${message}`, { cause });
}
await new Promise((resolve) => setTimeout(resolve, attempt * 1_000));
}
}
}
async function getJson(url: string, signal?: AbortSignal): Promise<unknown> {
const response = await fetch(url, {
signal: signal ?? AbortSignal.timeout(30_000),
cache: "no-store",
});
if (!response.ok)
throw new Error(`Lunaris returned ${response.status} for ${url}`);
return response.json();
return retryTimeout(`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",
});
if (!response.ok)
throw new Error(`Lunaris returned ${response.status} for ${url}`);
return response.json();
}, signal);
}
async function getOptionalJson(url: string, signal?: AbortSignal): Promise<unknown | null> {
const response = await fetch(url, {
signal: signal ?? AbortSignal.timeout(30_000),
cache: "no-store",
});
if (response.status === 404) return null;
if (!response.ok)
throw new Error(`Lunaris returned ${response.status} for ${url}`);
return response.json();
return retryTimeout(`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",
});
if (response.status === 404) return null;
if (!response.ok)
throw new Error(`Lunaris returned ${response.status} for ${url}`);
return response.json();
}, signal);
}
function webpName(value: string): string {
@@ -222,27 +242,29 @@ async function uploadAssets(
if (index >= assets.length) return;
await control?.checkCancelled();
const asset = assets[index];
if (!(await storage.file(asset.target).exists())) {
if (asset.body) {
await storage.write(asset.target, asset.body, {
type: asset.type ?? "image/webp",
acl: "public-read",
});
} else {
const response = await fetch(asset.source!, {
signal: AbortSignal.timeout(30_000),
cache: "no-store",
});
if (response.status === 404) missing.add(asset.target);
else if (!response.ok)
throw new Error(`Asset returned ${response.status}: ${asset.source}`);
else
await storage.write(asset.target, response, {
await retryTimeout(`Asset ${asset.target}`, async () => {
if (!(await storage.file(asset.target).exists())) {
if (asset.body) {
await storage.write(asset.target, asset.body, {
type: asset.type ?? "image/webp",
acl: "public-read",
});
} else {
const response = await fetch(asset.source!, {
signal: control?.signal ? AbortSignal.any([control.signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
cache: "no-store",
});
if (response.status === 404) missing.add(asset.target);
else if (!response.ok)
throw new Error(`Asset returned ${response.status}: ${asset.source}`);
else
await storage.write(asset.target, response, {
type: asset.type ?? "image/webp",
acl: "public-read",
});
}
}
}
}, control?.signal);
completed += 1;
await reportProgress();
}
@@ -256,10 +278,10 @@ async function uploadAssets(
async function syncCatalogVersion(version: string, control?: SyncControl): Promise<void> {
await control?.report("lists", 0, 4, "กำลังดึงรายการ Catalog จาก Lunaris");
const [charactersRaw, weaponsRaw, artifactsRaw, materialsRaw] = await Promise.all([
getJson(`${API_ROOT}/${version}/charlist.json`),
getJson(`${API_ROOT}/${version}/weaponlist.json`),
getJson(`${API_ROOT}/${version}/artifactlist.json`),
getJson(`${API_ROOT}/${version}/materiallist.json`),
getJson(`${API_ROOT}/${version}/charlist.json`, control?.signal),
getJson(`${API_ROOT}/${version}/weaponlist.json`, control?.signal),
getJson(`${API_ROOT}/${version}/artifactlist.json`, control?.signal),
getJson(`${API_ROOT}/${version}/materiallist.json`, control?.signal),
]);
await control?.report("lists", 4, 4, "ได้รับรายการ Catalog แล้ว");
const characterData = characterSchema.parse(charactersRaw);
@@ -288,6 +310,7 @@ async function syncCatalogVersion(version: string, control?: SyncControl): Promi
async ([key, value]) => {
const detailRaw = await getOptionalJson(
`${API_ROOT}/${version}/en/char/${encodeURIComponent(key)}.json`,
control?.signal,
);
const detail = detailRaw
? characterDetailSchema.parse(detailRaw)
@@ -337,6 +360,7 @@ async function syncCatalogVersion(version: string, control?: SyncControl): Promi
const detail = characterDetailSchema.parse(
await getJson(
`${API_ROOT}/${version}/en/char/${traveler.detailKey}.json`,
control?.signal,
),
);
return {
@@ -496,6 +520,7 @@ async function syncCatalogVersion(version: string, control?: SyncControl): Promi
weapons = weapons.filter((item) => !missingAssets.has(item.imageKey));
artifacts = artifacts.filter((item) => !missingAssets.has(item.imageKey));
await control?.checkCancelled();
const storage = await getMediaStorage();
await Promise.all([
storage.write(
@@ -552,6 +577,7 @@ async function syncCatalogVersion(version: string, control?: SyncControl): Promi
),
]);
await control?.checkCancelled();
await control?.report("database", 0, 1, "กำลังบันทึก Catalog");
await getDb().transaction(async (tx) => {
await tx.delete(catalogCharacters);