feat: improve assets caching with LRU
This commit is contained in:
+38
-12
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user