feat : update
This commit is contained in:
@@ -83,11 +83,13 @@ describe("Lunaris character data", () => {
|
||||
|
||||
it("normalizes leveling materials by category", () => {
|
||||
const detail = characterDetailSchema.parse({
|
||||
leveling: {
|
||||
crown: { "104319": 3 },
|
||||
books: { "104365": 9, "104366": 63, "104367": 114 },
|
||||
worldmonster: { "112146": 18, "112147": 66, "112148": 93 },
|
||||
weekly: { "113088": 18 },
|
||||
skills: {
|
||||
leveling: {
|
||||
crown: { "104319": 3 },
|
||||
books: { "104365": 9, "104366": 63, "104367": 114 },
|
||||
worldmonster: { "112146": 18, "112147": 66, "112148": 93 },
|
||||
weekly: { "113088": 18 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const skillSchema = z.object({
|
||||
});
|
||||
|
||||
const attributeSchema = z.record(z.string(), z.union([z.number(), z.string()]));
|
||||
const levelingSchema = z.record(z.string(), z.record(z.string(), z.number().int()));
|
||||
|
||||
export const characterDetailSchema = z.object({
|
||||
info: z.object({
|
||||
@@ -65,12 +66,13 @@ export const characterDetailSchema = z.object({
|
||||
normalattack: skillSchema.optional(),
|
||||
elementalskill: skillSchema.optional(),
|
||||
elementalburst: skillSchema.optional(),
|
||||
leveling: levelingSchema.optional(),
|
||||
}).nullish(),
|
||||
ascension: z.record(
|
||||
z.string(),
|
||||
z.union([z.number().int(), z.record(z.string(), z.number().int())]),
|
||||
).nullish(),
|
||||
leveling: z.record(z.string(), z.record(z.string(), z.number().int())).nullish(),
|
||||
leveling: levelingSchema.nullish(),
|
||||
attributes: z.array(attributeSchema).nullish(),
|
||||
});
|
||||
|
||||
@@ -157,7 +159,7 @@ export function ascensionSnapshot(
|
||||
export function levelingSnapshot(
|
||||
value: z.infer<typeof characterDetailSchema>,
|
||||
): CatalogLevelingGroup[] {
|
||||
return Object.entries(value.leveling ?? {}).map(([type, entries]) => ({
|
||||
return Object.entries(value.skills?.leveling ?? value.leveling ?? {}).map(([type, entries]) => ({
|
||||
type,
|
||||
items: Object.entries(entries).map(([key, amount]) => ({ key, amount })),
|
||||
}));
|
||||
|
||||
+58
-14
@@ -40,6 +40,14 @@ import { getMediaStorage } from "@/lib/media/storage";
|
||||
|
||||
const VERSION_URL = "https://api.lunaris.moe/data/version.json";
|
||||
const API_ROOT = "https://api.lunaris.moe/data";
|
||||
const PROGRESS_INTERVAL_MS = 400;
|
||||
|
||||
function syncConcurrency(): number {
|
||||
const configured = Number(process.env.CATALOG_SYNC_CONCURRENCY ?? 12);
|
||||
return Number.isFinite(configured)
|
||||
? Math.max(1, Math.min(24, Math.floor(configured)))
|
||||
: 12;
|
||||
}
|
||||
|
||||
const versionSchema = z.object({
|
||||
version: lunarisVersionSchema,
|
||||
@@ -160,16 +168,27 @@ async function mapConcurrent<T, R>(
|
||||
): Promise<R[]> {
|
||||
const results = new Array<R>(items.length);
|
||||
let next = 0;
|
||||
let completed = 0;
|
||||
let lastProgressAt = 0;
|
||||
async function reportProgress() {
|
||||
if (!control) return;
|
||||
const now = Date.now();
|
||||
if (completed < items.length && now - lastProgressAt < PROGRESS_INTERVAL_MS) return;
|
||||
lastProgressAt = now;
|
||||
await control.report("characters", completed, items.length, `ดึงข้อมูลตัวละคร ${completed}/${items.length}`);
|
||||
}
|
||||
async function worker() {
|
||||
while (next < items.length) {
|
||||
await control?.checkCancelled();
|
||||
while (true) {
|
||||
const index = next++;
|
||||
if (index >= items.length) return;
|
||||
await control?.checkCancelled();
|
||||
results[index] = await task(items[index]);
|
||||
await control?.report("characters", index + 1, items.length, `ดึงข้อมูลตัวละคร ${index + 1}/${items.length}`);
|
||||
completed += 1;
|
||||
await reportProgress();
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(4, items.length) }, () => worker()),
|
||||
Array.from({ length: Math.min(syncConcurrency(), items.length) }, () => worker()),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
@@ -185,11 +204,22 @@ async function uploadAssets(
|
||||
): Promise<Set<string>> {
|
||||
const storage = await getMediaStorage();
|
||||
let next = 0;
|
||||
let completed = 0;
|
||||
let lastProgressAt = 0;
|
||||
const missing = new Set<string>();
|
||||
async function reportProgress() {
|
||||
if (!control) return;
|
||||
const now = Date.now();
|
||||
if (completed < assets.length && now - lastProgressAt < PROGRESS_INTERVAL_MS) return;
|
||||
lastProgressAt = now;
|
||||
await control.report("assets", completed, assets.length, `ดาวน์โหลดรูปภาพ ${completed}/${assets.length}`);
|
||||
}
|
||||
async function worker() {
|
||||
while (next < assets.length) {
|
||||
while (true) {
|
||||
const index = next++;
|
||||
if (index >= assets.length) return;
|
||||
await control?.checkCancelled();
|
||||
const asset = assets[next++];
|
||||
const asset = assets[index];
|
||||
if (!(await storage.file(asset.target).exists())) {
|
||||
if (asset.body) {
|
||||
await storage.write(asset.target, asset.body, {
|
||||
@@ -211,11 +241,12 @@ async function uploadAssets(
|
||||
});
|
||||
}
|
||||
}
|
||||
await control?.report("assets", next, assets.length, `ดาวน์โหลดรูปภาพ ${next}/${assets.length}`);
|
||||
completed += 1;
|
||||
await reportProgress();
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(4, assets.length) }, () => worker()),
|
||||
Array.from({ length: Math.min(syncConcurrency(), assets.length) }, () => worker()),
|
||||
);
|
||||
return missing;
|
||||
}
|
||||
@@ -620,9 +651,20 @@ export async function startCatalogSyncJob(): Promise<CatalogSyncJob> {
|
||||
}
|
||||
|
||||
async function executeCatalogSyncJob(initial: CatalogSyncJob) {
|
||||
let lastCancellationCheck = 0;
|
||||
let cancellationCheck: Promise<void> | null = null;
|
||||
const checkCancelled = async () => {
|
||||
const current = await getCatalogSyncJob(initial.id);
|
||||
if (current?.status === "cancelled") throw new Error("CATALOG_SYNC_CANCELLED");
|
||||
if (Date.now() - lastCancellationCheck < PROGRESS_INTERVAL_MS) return;
|
||||
if (!cancellationCheck) {
|
||||
cancellationCheck = (async () => {
|
||||
const current = await getCatalogSyncJob(initial.id);
|
||||
lastCancellationCheck = Date.now();
|
||||
if (current?.status === "cancelled") throw new Error("CATALOG_SYNC_CANCELLED");
|
||||
})().finally(() => {
|
||||
cancellationCheck = null;
|
||||
});
|
||||
}
|
||||
await cancellationCheck;
|
||||
};
|
||||
const report = async (phase: string, completed: number, total: number, message: string) => {
|
||||
const current = await getCatalogSyncJob(initial.id);
|
||||
@@ -651,10 +693,12 @@ export async function getCurrentCatalogVersion(): Promise<string | null> {
|
||||
|
||||
async function hasCatalogRecords(): Promise<boolean> {
|
||||
const db = getDb();
|
||||
const [character] = await db.select({ key: catalogCharacters.key }).from(catalogCharacters).where(sql`${catalogCharacters.baseStats} <> '[]'::jsonb`).limit(1);
|
||||
const [material] = await db.select({ key: catalogMaterials.key }).from(catalogMaterials).limit(1);
|
||||
const [weapon] = await db.select({ key: catalogWeapons.key }).from(catalogWeapons).limit(1);
|
||||
const [artifact] = await db.select({ key: catalogArtifacts.key }).from(catalogArtifacts).limit(1);
|
||||
const [[character], [material], [weapon], [artifact]] = await Promise.all([
|
||||
db.select({ key: catalogCharacters.key }).from(catalogCharacters).where(sql`${catalogCharacters.baseStats} <> '[]'::jsonb AND ${catalogCharacters.leveling} <> '[]'::jsonb`).limit(1),
|
||||
db.select({ key: catalogMaterials.key }).from(catalogMaterials).limit(1),
|
||||
db.select({ key: catalogWeapons.key }).from(catalogWeapons).limit(1),
|
||||
db.select({ key: catalogArtifacts.key }).from(catalogArtifacts).limit(1),
|
||||
]);
|
||||
return Boolean(character && material && weapon && artifact);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "./validation";
|
||||
|
||||
describe("media validation", () => {
|
||||
it("accepts only supported images no larger than 20 MB", () => {
|
||||
it("accepts only supported images no larger than 100 MB", () => {
|
||||
expect(mediaUploadSchema.safeParse({ fileName: "a.png", mimeType: "image/png", byteSize: MAX_MEDIA_BYTES }).success).toBe(true);
|
||||
expect(mediaUploadSchema.safeParse({ fileName: "a.svg", mimeType: "image/svg+xml", byteSize: 10 }).success).toBe(false);
|
||||
expect(mediaUploadSchema.safeParse({ fileName: "a.png", mimeType: "image/png", byteSize: MAX_MEDIA_BYTES + 1 }).success).toBe(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as z from "zod";
|
||||
|
||||
export const MAX_MEDIA_BYTES = 20 * 1024 * 1024;
|
||||
export const MAX_MEDIA_BYTES = 100 * 1024 * 1024;
|
||||
export const IMAGE_MIME_TYPES = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
|
||||
Reference in New Issue
Block a user