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 { file, hash, write } from "bun";
|
||||||
import { TTLCache } from "enka.ts";
|
import { TTLCache } from "enka.ts";
|
||||||
import { AvatarStore, EnkaLocs, RelicStore, WeaponStore } from "@/type/enka";
|
import { AvatarStore, EnkaLocs, RelicStore, WeaponStore } from "@/type/enka";
|
||||||
|
import { Deduplicator } from "./dedupe";
|
||||||
|
|
||||||
export const cache = new TTLCache();
|
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>();
|
||||||
|
|
||||||
async getMissingAssets() {
|
async getMissingAssets() {
|
||||||
const avatars = await getEnkaAvatarStore();
|
const avatars = await getEnkaAvatarStore();
|
||||||
@@ -51,8 +53,10 @@ export class AssetManager {
|
|||||||
const local = file(join("store", hash(path).toString(36)));
|
const local = file(join("store", hash(path).toString(36)));
|
||||||
if (await local.exists()) return local.arrayBuffer();
|
if (await local.exists()) return local.arrayBuffer();
|
||||||
|
|
||||||
const data = await fetch(new URL(path, "https://enka.network")).then(
|
const data = await this.deduper.acquire(path, () =>
|
||||||
(res) => res.arrayBuffer(),
|
fetch(new URL(path, "https://enka.network")).then((res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
this.cache.set(path, data);
|
this.cache.set(path, data);
|
||||||
queueMicrotask(() => write(local, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-28
@@ -15,16 +15,20 @@ import type { App } from "@";
|
|||||||
import { generateBackground } from "@/assets/background";
|
import { generateBackground } from "@/assets/background";
|
||||||
import { ConstsSvg } from "@/assets/consts";
|
import { ConstsSvg } from "@/assets/consts";
|
||||||
import { featherImage } from "@/assets/feather";
|
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 shadeSvg from "@/assets/shade.svg" with { type: "file" };
|
||||||
import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
|
import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
|
||||||
import { star } from "@/assets/stars";
|
import { star } from "@/assets/stars";
|
||||||
import { TalentSvg } from "@/assets/talent";
|
import { TalentSvg } from "@/assets/talent";
|
||||||
import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
|
import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
|
||||||
import { AssetManager, getEnkaStores } from "@/lib/assets";
|
import { AssetManager, getEnkaStores } from "@/lib/assets";
|
||||||
|
import { Deduplicator } from "@/lib/dedupe";
|
||||||
import { mockData } from "@/lib/mock";
|
import { mockData } from "@/lib/mock";
|
||||||
|
|
||||||
const assets = new AssetManager();
|
const assets = new AssetManager();
|
||||||
const cache = new TTLCache();
|
const cache = new TTLCache();
|
||||||
|
const deduper = new Deduplicator();
|
||||||
|
|
||||||
const font = file(fontFile);
|
const font = file(fontFile);
|
||||||
const shade = file(shadeSvg);
|
const shade = file(shadeSvg);
|
||||||
@@ -44,14 +48,18 @@ export function card(app: App) {
|
|||||||
// const data = mockData;
|
// const data = mockData;
|
||||||
const [[avatarStore, weaponStore, relicStore, locs], data] =
|
const [[avatarStore, weaponStore, relicStore, locs], data] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
deduper.acquire("stores", () =>
|
||||||
getEnkaStores().finally(() =>
|
getEnkaStores().finally(() =>
|
||||||
console.log(`\n${Date.now() - start}ms - Stores loaded`),
|
console.log(`\n${Date.now() - start}ms - Stores loaded`),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
feature("API")
|
feature("API")
|
||||||
? fetchEnka(uid, false).finally(() =>
|
? deduper.acquire(`enka-${uid}`, () =>
|
||||||
|
fetchEnka(uid, false).finally(() =>
|
||||||
console.log(
|
console.log(
|
||||||
`${Date.now() - start}ms - Enka data loaded`,
|
`${Date.now() - start}ms - Enka data loaded`,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: mockData,
|
: mockData,
|
||||||
]);
|
]);
|
||||||
@@ -71,12 +79,11 @@ export function card(app: App) {
|
|||||||
|
|
||||||
if (!char.equipList)
|
if (!char.equipList)
|
||||||
return status(501, "INTERNAL: No equipList provided");
|
return status(501, "INTERNAL: No equipList provided");
|
||||||
|
const weaponRaw = char.equipList.find((e) => e.weapon)!;
|
||||||
const weapon = {
|
const weapon = {
|
||||||
data: char.equipList.find((e) => e.weapon)!.weapon!,
|
data: weaponRaw.weapon!,
|
||||||
store:
|
flat: weaponRaw.flat,
|
||||||
weaponStore[
|
store: weaponStore[weaponRaw.itemId ?? "Unknown"],
|
||||||
char.equipList.find((e) => e.weapon)!.itemId ?? "Unknown"
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheKey = hash(JSON.stringify(data)).toString(36);
|
const cacheKey = hash(JSON.stringify(data)).toString(36);
|
||||||
@@ -264,7 +271,7 @@ export function card(app: App) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* middle section */}
|
{/* 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 */}
|
{/* weapon */}
|
||||||
<div tw="flex flex-start" className="Weapon">
|
<div tw="flex flex-start" className="Weapon">
|
||||||
{/* display */}
|
{/* display */}
|
||||||
@@ -289,7 +296,7 @@ export function card(app: App) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* caption */}
|
{/* caption */}
|
||||||
<div tw="px-[5%] grow text-white self-center">
|
<div tw="px-[5%] grow text-white self-center flex flex-col">
|
||||||
<div
|
<div
|
||||||
tw="text-[108%] leading-[1.3]"
|
tw="text-[108%] leading-[1.3]"
|
||||||
style={{
|
style={{
|
||||||
@@ -300,7 +307,17 @@ export function card(app: App) {
|
|||||||
<span>{locs.en[weapon.store.NameTextMapHash]}</span>
|
<span>{locs.en[weapon.store.NameTextMapHash]}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* stats */}
|
{/* 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +334,7 @@ export function card(app: App) {
|
|||||||
format: "webp",
|
format: "webp",
|
||||||
// drawDebugBorder: true,
|
// drawDebugBorder: true,
|
||||||
stylesheets: [
|
stylesheets: [
|
||||||
`
|
/* css */ `
|
||||||
.Rock { color: #ffc64a; }
|
.Rock { color: #ffc64a; }
|
||||||
.Talents.Rock { color: #4a3a18!important }
|
.Talents.Rock { color: #4a3a18!important }
|
||||||
.Wind { color: #71fdec; }
|
.Wind { color: #71fdec; }
|
||||||
@@ -332,24 +349,6 @@ export function card(app: App) {
|
|||||||
.Talents.Fire { color: #541900!important }
|
.Talents.Fire { color: #541900!important }
|
||||||
.Grass { color: #a5ffab; }
|
.Grass { color: #a5ffab; }
|
||||||
.Talents.Grass { color: #134517!important }
|
.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