feat(assets): AssetManager parallel fetch
Build and Deploy / build-and-deploy (push) Successful in 1m18s

- max 40/s, 20 concurrent
This commit is contained in:
2026-07-10 15:09:30 +07:00 Unverified
parent f1729a03bb
commit e2d156e552
+34 -11
View File
@@ -23,6 +23,7 @@ import { throwNotOk } from "@/lib/util";
globalThis.devCache ??= new TTLCache(); globalThis.devCache ??= new TTLCache();
export const cache = globalThis.devCache; export const cache = globalThis.devCache;
const log = createLogger("ASSET"); const log = createLogger("ASSET");
const storeLog = createLogger("STORE");
export class AssetManager { export class AssetManager {
private cache = new Map<string, ArrayBuffer>(); private cache = new Map<string, ArrayBuffer>();
@@ -58,27 +59,49 @@ export class AssetManager {
async getMissingAssets() { async getMissingAssets() {
await mkdir("store").catch(() => {}); await mkdir("store").catch(() => {});
const existing = new Set(await readdir("store")); const existing = new Set(await readdir("store"));
log`Initializing AssetManager`;
const missing = new Set<string>(); const missing = new Set<string>();
(await this.getAssetList()).map( const list = await this.getAssetList();
(n) => existing.has(hash(n).toString(36)) || missing.add(n), list.map((n) => existing.has(hash(n).toString(36)) || missing.add(n));
); const listDeduplicated = new Set(list);
const queue = [...missing.values()]
.map((path) => ({ path, tries: 0 }))
.sort(() => Math.random() - 0.5);
var running = 0;
log.warn`Missing: ${missing.size}\nIn store: ${existing.size}\nTotal: ${list.length}/${listDeduplicated.size}`;
log`${missing.size} assets missing, ${existing.size} assets in store`; const manager = setInterval(() => {
if (queue.length === 0 && running === 0) {
log`All missing assets fetched and stored`;
return clearInterval(manager);
}
for (const name of missing) if (running >= 20) return;
await fetch(new URL(name, "https://enka.network")) const q = queue.pop();
if (!q) return;
running++;
fetch(new URL(q.path, "https://enka.network"))
.then((res) => { .then((res) => {
if (res.status === 404) { if (res.status === 404) {
console.warn(`Asset not found: ${name}`); log.warn`Asset not found: ${q.path}`;
return new ArrayBuffer(0); return new ArrayBuffer(0);
} }
if (!res.ok) if (!res.ok)
throw new Error(`Failed to fetch ${name}: ${res.statusText}`); throw new Error(`Failed to fetch ${q.path}: ${res.statusText}`);
return res.arrayBuffer(); return res.arrayBuffer();
}) })
.then((d) => write(join("store", hash(name).toString(36)), d)); .then((d) => write(join("store", hash(q.path).toString(36)), d))
.then(() => log`${q.path} stored. ${queue.length + running} left.`)
.catch((e) => {
log.error`${e}`;
q.tries++;
queue.push(q);
})
.finally(() => running--);
}, 1000 / 40);
log`All missing assets fetched and stored`; // for (const name of missing);
} }
async purgeExtraAssets() { async purgeExtraAssets() {
@@ -203,7 +226,7 @@ export async function getEnkaStore<K extends keyof StoreTypeMap>(
const cached = cache.get(`store.${key}`); const cached = cache.get(`store.${key}`);
if (cached) return cached as StoreTypeMap[K]; if (cached) return cached as StoreTypeMap[K];
log`Fresh: store.${key}`; storeLog`Fresh: store.${key}`;
const response = await fetch( const response = await fetch(
`https://api.enka.network/store/gi/${key}.json`, `https://api.enka.network/store/gi/${key}.json`,
).then(throwNotOk); ).then(throwNotOk);