feat: improve assets caching with LRU

This commit is contained in:
2026-03-20 16:05:25 +07:00 Unverified
parent 9132417568
commit cfa75dfddf
+38 -12
View File
@@ -10,13 +10,14 @@ export const cache = new TTLCache();
export class AssetManager { export class AssetManager {
private cache = new Map<string, ArrayBuffer>(); private cache = new Map<string, ArrayBuffer>();
private deduper = new Deduplicator<ArrayBuffer>(); private deduper = new Deduplicator<ArrayBuffer>();
private MAX = 300;
async getMissingAssets() { async getMissingAssets() {
const avatars = await getEnkaAvatarStore(); const avatars = await getEnkaAvatarStore();
const weapons = await getEnkaWeaponStore(); const weapons = await getEnkaWeaponStore();
const relics = await getEnkaRelicStore(); const relics = await getEnkaRelicStore();
await mkdir("store").catch(() => {}); await mkdir("store").catch(() => {});
const existing = await readdir("store"); const existing = new Set(await readdir("store"));
const missing = new Set<string>(); const missing = new Set<string>();
[ [
...Object.values(avatars).flatMap((a) => [ ...Object.values(avatars).flatMap((a) => [
@@ -27,10 +28,10 @@ export class AssetManager {
]), ]),
...Object.values(weapons).flatMap((w) => [w.Icon, w.AwakenIcon]), ...Object.values(weapons).flatMap((w) => [w.Icon, w.AwakenIcon]),
...Object.values(relics.Items).map((r) => r.Icon), ...Object.values(relics.Items).map((r) => r.Icon),
].map((n) => existing.includes(hash(n).toString(36)) || missing.add(n)); ].map((n) => existing.has(hash(n).toString(36)) || missing.add(n));
console.log( console.log(
`${missing.size} assets missing, ${existing.length} assets in store`, `${missing.size} assets missing, ${existing.size} assets in store`,
); );
for (const name of missing) for (const name of missing)
@@ -44,22 +45,27 @@ export class AssetManager {
throw new Error(`Failed to fetch ${name}: ${res.statusText}`); throw new Error(`Failed to fetch ${name}: ${res.statusText}`);
return res.arrayBuffer(); return res.arrayBuffer();
}) })
.then((d) => write(`store/${hash(name).toString(36)}`, d)); .then((d) => write(join("store", hash(name).toString(36)), d));
console.log("All missing assets fetched and stored"); console.log("All missing assets fetched and stored");
} }
async load(path: string) { async load(path: string) {
if (this.cache.has(path)) return this.cache.get(path)!; const cacheKey = join("store", hash(path).toString(36));
const local = file(join("store", hash(path).toString(36))); if (this.cache.has(cacheKey)) return this.cache.get(cacheKey)!;
if (await local.exists()) return local.arrayBuffer(); const local = file(cacheKey);
if (await local.exists()) {
const data = await local.arrayBuffer();
this.touch(cacheKey, data);
return data;
}
const data = await this.deduper.acquire(path, () => const data = await this.deduper.acquire(cacheKey, () =>
fetch(new URL(path, "https://enka.network")).then((res) => fetch(new URL(path, "https://enka.network")).then((res) =>
res.arrayBuffer(), res.arrayBuffer(),
), ),
); );
this.cache.set(path, data); this.touch(cacheKey, data);
queueMicrotask(() => write(local, data)); queueMicrotask(() => write(local, data));
return data; return data;
} }
@@ -84,18 +90,38 @@ export class AssetManager {
const cacheKey = join("store", hash(`${key}-fnCache`).toString(36)); const cacheKey = join("store", hash(`${key}-fnCache`).toString(36));
if (this.cache.has(cacheKey)) return this.cache.get(cacheKey)!; if (this.cache.has(cacheKey)) return this.cache.get(cacheKey)!;
const cached = file(cacheKey); const cached = file(cacheKey);
if (await cached.exists()) return cached.arrayBuffer(); if (await cached.exists()) {
const data = await cached.arrayBuffer();
this.touch(cacheKey, data);
return data;
}
const returned = await get(); const returned = await get();
this.cache.set( this.touch(
cacheKey, cacheKey,
returned instanceof ArrayBuffer returned instanceof ArrayBuffer
? returned ? returned
: (returned.buffer as ArrayBuffer), : (returned.buffer.slice(
returned.byteOffset,
returned.byteOffset + returned.byteLength,
) as ArrayBuffer),
); );
queueMicrotask(() => write(file(cacheKey), returned)); queueMicrotask(() => write(file(cacheKey), returned));
return returned; return returned;
} }
// a simple LRU
private touch(key: string, value: ArrayBuffer) {
// move to newest
this.cache.delete(key);
this.cache.set(key, value);
// purge oldest
if (this.cache.size > this.MAX) {
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest!);
}
}
} }
export async function getEnkaStores() { export async function getEnkaStores() {