chore: cleanup

This commit is contained in:
2026-03-20 16:26:56 +07:00 Unverified
parent cfa75dfddf
commit 7a76f9971c
5 changed files with 92 additions and 69 deletions
+42 -11
View File
@@ -1,13 +1,44 @@
import process from "node:process"; import openapi from "@elysiajs/openapi";
import { AssetManager } from "./lib/assets"; import { Elysia, t } from "elysia";
import { version } from "../package.json";
import { pm } from "./lib/auth";
import { v1routes } from "./v1";
queueMicrotask(async () => { const app = new Elysia()
const assets = new AssetManager(); .use(
assets.getMissingAssets(); openapi({
exclude: { paths: ["/", "/health"] },
documentation: { info: { title: "Astral API", version } },
path: "/docs",
}),
)
.guard({
headers: t.Object({
authorization: t.Optional(
t.String({
pattern: "^Bearer [a-zA-Z0-9-_+=]+",
description: "API Key",
}),
),
}),
})
.derive(({ headers }) => {
const auth = headers.authorization;
const bearer = auth?.startsWith("Bearer ") ? auth.slice(7) : null;
setInterval(() => { return {
assets.getMissingAssets(); bearer,
}, 86400000); // refresh assets every 24 hours key: bearer ? pm.validate(bearer) : pm.invalid,
}); };
await import("./server"); })
console.log(`Worker ${process.pid} started`); .get("/", ({ redirect }) => redirect("/docs"))
.get("/health", () => "OK")
.listen(3000);
v1routes(app);
export type App = typeof app;
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);
+15 -3
View File
@@ -1,3 +1,4 @@
import { readdirSync } from "node:fs";
import { mkdir, readdir } from "node:fs/promises"; import { mkdir, readdir } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { file, hash, write } from "bun"; import { file, hash, write } from "bun";
@@ -11,6 +12,7 @@ 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; private MAX = 300;
private approximateSize = 0;
async getMissingAssets() { async getMissingAssets() {
const avatars = await getEnkaAvatarStore(); const avatars = await getEnkaAvatarStore();
@@ -112,16 +114,26 @@ export class AssetManager {
// a simple LRU // a simple LRU
private touch(key: string, value: ArrayBuffer) { private touch(key: string, value: ArrayBuffer) {
if (!this.cache.has(key)) this.approximateSize += value.byteLength;
// move to newest // move to newest
this.cache.delete(key); this.cache.delete(key);
this.cache.set(key, value); this.cache.set(key, value);
// purge oldest
if (this.cache.size > this.MAX) { if (this.cache.size > this.MAX) {
const oldest = this.cache.keys().next().value; // purge oldest
this.cache.delete(oldest!); const oldest = this.cache.keys().next().value!;
this.approximateSize -= this.cache.get(oldest)?.byteLength ?? 0;
this.cache.delete(oldest);
} }
} }
get health() {
return {
lru: this.cache.size,
lruSize: `${(this.approximateSize / 1000).toFixed(2)}Kb`,
disk: readdirSync("store").length,
};
}
} }
export async function getEnkaStores() { export async function getEnkaStores() {
-44
View File
@@ -1,44 +0,0 @@
import openapi from "@elysiajs/openapi";
import { Elysia, t } from "elysia";
import { version } from "../package.json";
import { pm } from "./lib/auth";
import { v1routes } from "./v1";
const app = new Elysia()
.use(
openapi({
exclude: { paths: ["/", "/health"] },
documentation: { info: { title: "Astral API", version } },
path: "/docs",
}),
)
.guard({
headers: t.Object({
authorization: t.Optional(
t.String({
pattern: "^Bearer [a-zA-Z0-9-_+=]+",
description: "API Key",
}),
),
}),
})
.derive(({ headers }) => {
const auth = headers.authorization;
const bearer = auth?.startsWith("Bearer ") ? auth.slice(7) : null;
return {
bearer,
key: bearer ? pm.validate(bearer) : pm.invalid,
};
})
.get("/", ({ redirect }) => redirect("/docs"))
.get("/health", () => "OK")
.listen(3000);
v1routes(app);
export type App = typeof app;
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);
+34 -10
View File
@@ -11,7 +11,6 @@ import {
TTLCache, TTLCache,
} from "enka.ts"; } from "enka.ts";
import { twj } from "tw-to-css"; import { twj } from "tw-to-css";
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";
@@ -22,6 +21,7 @@ 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 type { App } from "@/index";
import { AssetManager, getEnkaStores } from "@/lib/assets"; import { AssetManager, getEnkaStores } from "@/lib/assets";
import { Deduplicator } from "@/lib/dedupe"; import { Deduplicator } from "@/lib/dedupe";
import { mockData } from "@/lib/mock"; import { mockData } from "@/lib/mock";
@@ -35,6 +35,15 @@ const deduper = new Deduplicator();
const font = file(fontFile); const font = file(fontFile);
const shade = file(shadeSvg); const shade = file(shadeSvg);
queueMicrotask(async () => {
const assets = new AssetManager();
assets.getMissingAssets();
setInterval(() => {
assets.getMissingAssets();
}, 86400000); // refresh assets every 24 hours
});
export function card(app: App) { export function card(app: App) {
return app return app
.group("/card", (app) => .group("/card", (app) =>
@@ -200,15 +209,30 @@ export function card(app: App) {
const statRows = buildDisplayRows(char, locs); const statRows = buildDisplayRows(char, locs);
if (debug) if (debug) {
return JSON.stringify({ console.log(
char, `${Date.now() - start}ms - Debug endpoint, returning JSON`,
weapon, );
assets: resources.map((e) => e.src), return JSON.stringify(
charLvl, {
charLvlMax, charLvl,
statRows, charLvlMax,
}); cacheKey,
cache: assets.health,
assets: Object.fromEntries(
resources.map((e) => [
e.src,
`${(e.data.byteLength / 1000).toFixed(2)}Kb`,
]),
),
char,
weapon,
statRows,
},
null,
2,
);
}
const res = new ImageResponse( const res = new ImageResponse(
<div tw="relative text-[32.25px] text-white rounded-xl overflow-hidden"> <div tw="relative text-[32.25px] text-white rounded-xl overflow-hidden">
+1 -1
View File
@@ -103,7 +103,7 @@
"skipLibCheck": true /* Skip type checking all .d.ts files. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */,
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@": ["./src/server.ts"] "@": ["./src/index.ts"]
} }
} }
} }