feat: add Deduplicator class for managing unique promises and implement icon library generator
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+6
-2
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
`,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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")}
|
||||
};`,
|
||||
);
|
||||
Reference in New Issue
Block a user