feat : let discord fetcher can stop working and accept every message type
This commit is contained in:
+63
-37
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user