feat: add Deduplicator class for managing unique promises and implement icon library generator

This commit is contained in:
2026-03-17 16:07:24 +07:00 Unverified
parent b28ac7b9db
commit 54c8f48a4f
7 changed files with 3247 additions and 34 deletions
+852
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+6 -2
View File
@@ -3,11 +3,13 @@ import { join } from "node:path";
import { file, hash, write } from "bun";
import { TTLCache } from "enka.ts";
import { AvatarStore, EnkaLocs, RelicStore, WeaponStore } from "@/type/enka";
import { Deduplicator } from "./dedupe";
export const cache = new TTLCache();
export class AssetManager {
private cache = new Map<string, ArrayBuffer>();
private deduper = new Deduplicator<ArrayBuffer>();
async getMissingAssets() {
const avatars = await getEnkaAvatarStore();
@@ -51,8 +53,10 @@ export class AssetManager {
const local = file(join("store", hash(path).toString(36)));
if (await local.exists()) return local.arrayBuffer();
const data = await fetch(new URL(path, "https://enka.network")).then(
(res) => res.arrayBuffer(),
const data = await this.deduper.acquire(path, () =>
fetch(new URL(path, "https://enka.network")).then((res) =>
res.arrayBuffer(),
),
);
this.cache.set(path, data);
queueMicrotask(() => write(local, data));
+24
View File
@@ -0,0 +1,24 @@
export class Deduplicator<T> {
// biome-ignore lint/suspicious/noExplicitAny: This is a generic class, so we need to use any here.
private map: Map<string, Promise<any>> = new Map();
has(key: string): boolean {
return this.map.has(key);
}
get<C = T>(key: string): Promise<C> | undefined {
return this.map.get(key);
}
acquire<C = T>(key: string, factory: () => Promise<C>): Promise<C> {
if (this.map.has(key)) {
return this.map.get(key)!;
}
const value = factory();
this.map.set(key, value);
value.finally(() => {
this.map.delete(key);
});
return value;
}
}
+31 -32
View File
@@ -15,16 +15,20 @@ import type { App } from "@";
import { generateBackground } from "@/assets/background";
import { ConstsSvg } from "@/assets/consts";
import { featherImage } from "@/assets/feather";
import { EnkaIcon } from "@/assets/icons";
import { library } from "@/assets/icons-library";
import shadeSvg from "@/assets/shade.svg" with { type: "file" };
import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
import { star } from "@/assets/stars";
import { TalentSvg } from "@/assets/talent";
import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
import { AssetManager, getEnkaStores } from "@/lib/assets";
import { Deduplicator } from "@/lib/dedupe";
import { mockData } from "@/lib/mock";
const assets = new AssetManager();
const cache = new TTLCache();
const deduper = new Deduplicator();
const font = file(fontFile);
const shade = file(shadeSvg);
@@ -44,13 +48,17 @@ export function card(app: App) {
// const data = mockData;
const [[avatarStore, weaponStore, relicStore, locs], data] =
await Promise.all([
getEnkaStores().finally(() =>
console.log(`\n${Date.now() - start}ms - Stores loaded`),
deduper.acquire("stores", () =>
getEnkaStores().finally(() =>
console.log(`\n${Date.now() - start}ms - Stores loaded`),
),
),
feature("API")
? fetchEnka(uid, false).finally(() =>
console.log(
`${Date.now() - start}ms - Enka data loaded`,
? deduper.acquire(`enka-${uid}`, () =>
fetchEnka(uid, false).finally(() =>
console.log(
`${Date.now() - start}ms - Enka data loaded`,
),
),
)
: mockData,
@@ -71,12 +79,11 @@ export function card(app: App) {
if (!char.equipList)
return status(501, "INTERNAL: No equipList provided");
const weaponRaw = char.equipList.find((e) => e.weapon)!;
const weapon = {
data: char.equipList.find((e) => e.weapon)!.weapon!,
store:
weaponStore[
char.equipList.find((e) => e.weapon)!.itemId ?? "Unknown"
],
data: weaponRaw.weapon!,
flat: weaponRaw.flat,
store: weaponStore[weaponRaw.itemId ?? "Unknown"],
};
const cacheKey = hash(JSON.stringify(data)).toString(36);
@@ -264,7 +271,7 @@ export function card(app: App) {
</div>
</div>
{/* middle section */}
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em] flex flex-col">
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[25.5em] p-[1.4em_.7em_.7em] flex flex-col">
{/* weapon */}
<div tw="flex flex-start" className="Weapon">
{/* display */}
@@ -289,7 +296,7 @@ export function card(app: App) {
</div>
</div>
{/* caption */}
<div tw="px-[5%] grow text-white self-center">
<div tw="px-[5%] grow text-white self-center flex flex-col">
<div
tw="text-[108%] leading-[1.3]"
style={{
@@ -300,7 +307,17 @@ export function card(app: App) {
<span>{locs.en[weapon.store.NameTextMapHash]}</span>
</div>
{/* stats */}
<div tw="pt-[3%] pb-[2%]"></div>
<div tw="pt-[3%] pb-[2%] flex gap-[.5em]">
{weapon.flat.weaponStats?.map((s) => (
<div tw="rounded-[.2em] leading-none bg-white/15 px-[.34em] py-[0.25em] flex items-center gap-[.7em] text-[1.03em]">
<EnkaIcon
icon={s.appendPropId as keyof typeof library}
tw="h-[1.1em]"
/>
<div tw="pt-[.1em]">{s.statValue}</div>
</div>
))}
</div>
</div>
</div>
</div>
@@ -317,7 +334,7 @@ export function card(app: App) {
format: "webp",
// drawDebugBorder: true,
stylesheets: [
`
/* css */ `
.Rock { color: #ffc64a; }
.Talents.Rock { color: #4a3a18!important }
.Wind { color: #71fdec; }
@@ -332,24 +349,6 @@ export function card(app: App) {
.Talents.Fire { color: #541900!important }
.Grass { color: #a5ffab; }
.Talents.Grass { color: #134517!important }
.Substat {
display: inline-block;
background: rgba(255,255,255,.15);
border-radius: .2em;
padding: 2% 3%;
box-sizing: content-box;
margin-right: .5em;
line-height: 1;
}
.Weapon .Substat:before {
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: .2em
}
`,
],
},
+53
View File
@@ -0,0 +1,53 @@
import { write } from "bun";
import { libraryRaw } from "@/assets/icons";
function toJsx(
name: string,
{ viewBox, svg }: { viewBox: string; svg: string },
) {
return ` "${name}": (props: SVGProps<SVGSVGElement>) => (<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="${viewBox}"
{...props}
>
<title>${name}</title>
${svg
.replaceAll("xlink:href", "xlinkHref")
.replace(
/<style([^>]*)>([^<]+)<\/style>/g,
(_, p, g) => `<style${p}>{${JSON.stringify(g)}}</style>`,
)
.replace(/class(="[^"]+")/g, "className$1")
.replace(
/style="([^"]+)"/g,
(_, g: string) =>
`style={${JSON.stringify(
Object.fromEntries(
g
.split(";")
.filter((e) => !e.startsWith("-"))
.map((r) => r.split(":"))
.map(([name, value]) => [
name.replace(/-([a-z])/g, (_, m: string) =>
m.toUpperCase(),
),
value,
]),
),
)}}`,
)}
</svg>),
`;
}
console.log(toJsx("Const", libraryRaw.Const));
write(
"result.tsx",
`import { SVGProps } from "react";
const library = {
${Object.entries(libraryRaw)
.map(([name, p]) => toJsx(name, p))
.join("\n")}
};`,
);