Skip to content

Commit fcbff1a

Browse files
committed
Cleanup loading
1 parent 5c772db commit fcbff1a

5 files changed

Lines changed: 108 additions & 91 deletions

File tree

src/data/derived.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,11 @@ function buildReferences(
6969
list.push(entry);
7070
}
7171

72+
const typeKeys = new Set<string>();
73+
7274
for (const decl of allDeclarations(declarations)) {
7375
if (decl.kind === "class") {
76+
const selfKey = declarationKey(decl.module, decl.name);
7477
for (const parent of decl.parents) {
7578
addRef(declarationKey(parent.module, parent.name), {
7679
declarationName: decl.name,
@@ -79,11 +82,10 @@ function buildReferences(
7982
});
8083
}
8184
for (const field of decl.fields) {
82-
const keys = new Set<string>();
83-
collectTypeKeys(field.type, keys);
84-
const declKey = declarationKey(decl.module, decl.name);
85-
for (const key of keys) {
86-
if (key !== declKey) {
85+
typeKeys.clear();
86+
collectTypeKeys(field.type, typeKeys);
87+
for (const key of typeKeys) {
88+
if (key !== selfKey) {
8789
addRef(key, {
8890
declarationName: decl.name,
8991
declarationModule: decl.module,

src/data/loader.ts

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,21 @@ const schemaUrls = import.meta.glob<string>("../../schemas/*.json.gz", {
77
eager: true,
88
});
99

10-
type LoadResult = Awaited<ReturnType<typeof parseSchemas>>;
11-
const cache = new Map<GameId, Promise<LoadResult>>();
10+
export async function loadGameSchemas(gameId: GameId) {
11+
const key = Object.keys(schemaUrls).find((k) => k.endsWith(`/${gameId}.json.gz`));
12+
if (!key) throw new Error(`No schema found for ${gameId}`);
13+
const url = schemaUrls[key];
14+
let response: Response;
15+
try {
16+
response = await fetch(url);
17+
} catch {
18+
throw new Error("Network request failed. Check your internet connection.");
19+
}
20+
if (!response.ok) throw new Error(`Server returned ${response.status} for ${url}`);
1221

13-
export function loadGameSchemas(gameId: GameId) {
14-
const cached = cache.get(gameId);
15-
if (cached) return cached;
22+
const data: SchemasJson = await new Response(
23+
response.body!.pipeThrough(new DecompressionStream("gzip")),
24+
).json();
1625

17-
const promise = (async () => {
18-
const key = Object.keys(schemaUrls).find((k) => k.endsWith(`/${gameId}.json.gz`));
19-
if (!key) throw new Error(`No schema found for ${gameId}`);
20-
const url = schemaUrls[key];
21-
let response: Response;
22-
try {
23-
response = await fetch(url);
24-
} catch {
25-
throw new Error("Network request failed. Check your internet connection.");
26-
}
27-
if (!response.ok) throw new Error(`Server returned ${response.status} for ${url}`);
28-
29-
const buf = await response.arrayBuffer();
30-
let data: SchemasJson;
31-
// If the first two bytes are the gzip magic number, decompress manually;
32-
// otherwise the browser already decompressed it via Content-Encoding.
33-
const magic = new Uint8Array(buf, 0, 2);
34-
if (magic[0] === 0x1f && magic[1] === 0x8b) {
35-
const decompressed = new Response(buf).body!.pipeThrough(new DecompressionStream("gzip"));
36-
data = await new Response(decompressed).json();
37-
} else {
38-
data = JSON.parse(new TextDecoder().decode(buf));
39-
}
40-
41-
return parseSchemas(data);
42-
})().catch((e) => {
43-
cache.delete(gameId);
44-
throw e;
45-
});
46-
47-
cache.set(gameId, promise);
48-
return promise;
26+
return parseSchemas(data);
4927
}

src/data/schemas.ts

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
Declaration,
3+
SchemaEnum,
34
SchemaClass,
45
SchemaField,
56
SchemaFieldType,
@@ -25,6 +26,27 @@ export function parseKV3Defaults(value: string): Record<string, unknown> | null
2526
return null;
2627
}
2728

29+
function deepEqual(a: unknown, b: unknown): boolean {
30+
if (a === b) return true;
31+
if (a === null || b === null || typeof a !== typeof b) return false;
32+
if (typeof a !== "object") return false;
33+
if (Array.isArray(a)) {
34+
if (!Array.isArray(b) || a.length !== b.length) return false;
35+
for (let i = 0; i < a.length; i++) {
36+
if (!deepEqual(a[i], b[i])) return false;
37+
}
38+
return true;
39+
}
40+
const aObj = a as Record<string, unknown>;
41+
const bObj = b as Record<string, unknown>;
42+
const aKeys = Object.keys(aObj);
43+
if (aKeys.length !== Object.keys(bObj).length) return false;
44+
for (const key of aKeys) {
45+
if (!deepEqual(aObj[key], bObj[key])) return false;
46+
}
47+
return true;
48+
}
49+
2850
/** @internal Exported for testing */
2951
export function diffObject(
3052
embedded: Record<string, unknown>,
@@ -34,7 +56,7 @@ export function diffObject(
3456
let hasDiff = false;
3557
for (const [k, v] of Object.entries(embedded)) {
3658
if (k === "_class" || v === HIDDEN_SENTINEL) continue;
37-
if (JSON.stringify(v) !== JSON.stringify(ownDefaults[k])) {
59+
if (!deepEqual(v, ownDefaults[k])) {
3860
diff[k] = v;
3961
hasDiff = true;
4062
}
@@ -62,25 +84,30 @@ function assignDefaults(classes: SchemaClass[]) {
6284
const classMap = new Map<string, SchemaClass>();
6385
for (const cls of classes) classMap.set(`${cls.module}/${cls.name}`, cls);
6486

65-
// Parse and cache defaults per class key
66-
const defaultsCache = new Map<string, Record<string, unknown> | null>();
87+
// Pre-parse all KV3 defaults before the main loop mutates metadata
88+
const parsedDefaults = new Map<string, Record<string, unknown> | null>();
89+
for (const cls of classes) {
90+
const meta = cls.metadata.find((m) => m.name === "MGetKV3ClassDefaults" && m.value);
91+
parsedDefaults.set(`${cls.module}/${cls.name}`, meta ? parseKV3Defaults(meta.value!) : null);
92+
}
6793
function getDefaults(module: string, name: string): Record<string, unknown> | null {
68-
const key = `${module}/${name}`;
69-
if (defaultsCache.has(key)) return defaultsCache.get(key)!;
70-
const cls = classMap.get(key);
71-
const meta = cls?.metadata.find((m) => m.name === "MGetKV3ClassDefaults" && m.value);
72-
const parsed = meta ? parseKV3Defaults(meta.value!) : null;
73-
defaultsCache.set(key, parsed);
74-
return parsed;
94+
return parsedDefaults.get(`${module}/${name}`) ?? null;
7595
}
7696

77-
// Collect all fields including from parent chain
78-
function collectFields(cls: SchemaClass, out: Map<string, SchemaField>) {
97+
// Cache all field names (including inherited) per class
98+
const allFieldsCache = new Map<string, Set<string>>();
99+
function getAllFieldNames(cls: SchemaClass): Set<string> {
100+
const key = `${cls.module}/${cls.name}`;
101+
const cached = allFieldsCache.get(key);
102+
if (cached !== undefined) return cached;
103+
const names = new Set<string>();
79104
for (const p of cls.parents) {
80105
const parent = classMap.get(`${p.module}/${p.name}`);
81-
if (parent) collectFields(parent, out);
106+
if (parent) for (const n of getAllFieldNames(parent)) names.add(n);
82107
}
83-
for (const f of cls.fields) out.set(f.name, f);
108+
for (const f of cls.fields) names.add(f.name);
109+
allFieldsCache.set(key, names);
110+
return names;
84111
}
85112

86113
for (const cls of classes) {
@@ -91,16 +118,13 @@ function assignDefaults(classes: SchemaClass[]) {
91118
const ownFields = new Map<string, SchemaField>();
92119
for (const f of cls.fields) ownFields.set(f.name, f);
93120

94-
// Track parent field names so we know which keys are inherited vs truly unknown
95-
const allFields = new Map<string, SchemaField>();
96-
collectFields(cls, allFields);
97-
121+
const allFieldNames = getAllFieldNames(cls);
98122
const unconsumed: Record<string, unknown> = {};
99123

100124
for (const [key, value] of Object.entries(defaults)) {
101125
if (value === HIDDEN_SENTINEL) continue;
102126

103-
if (!allFields.has(key)) {
127+
if (!allFieldNames.has(key)) {
104128
unconsumed[key] = value;
105129
continue;
106130
}
@@ -116,10 +140,7 @@ function assignDefaults(classes: SchemaClass[]) {
116140
break;
117141
}
118142
}
119-
if (
120-
parentDefault !== undefined &&
121-
JSON.stringify(value) !== JSON.stringify(parentDefault)
122-
) {
143+
if (parentDefault !== undefined && !deepEqual(value, parentDefault)) {
123144
unconsumed[key] = value;
124145
}
125146
continue;
@@ -200,30 +221,25 @@ export interface SchemaMetadata {
200221
export type ParsedSchemas = ReturnType<typeof parseSchemas>;
201222

202223
export function parseSchemas(data: SchemasJson) {
203-
const classes: Declaration[] = data.classes.map((c) => ({
204-
kind: "class" as const,
205-
name: c.name,
206-
module: c.module,
207-
parents: c.parents ?? [],
208-
fields: (c.fields ?? []).map((f) => ({
209-
...f,
210-
metadata: f.metadata ?? [],
211-
})),
212-
metadata: c.metadata ?? [],
213-
}));
214-
const enums: Declaration[] = data.enums.map((e) => ({
215-
kind: "enum" as const,
216-
name: e.name,
217-
module: e.module,
218-
alignment: e.alignment,
219-
members: (e.members ?? []).map((m) => ({
220-
...m,
221-
metadata: m.metadata ?? [],
222-
})),
223-
metadata: e.metadata ?? [],
224-
}));
224+
const classes = data.classes as SchemaClass[];
225+
for (const c of classes) {
226+
c.kind = "class";
227+
c.parents ??= [];
228+
c.metadata ??= [];
229+
for (const f of (c.fields ??= [])) {
230+
f.metadata ??= [];
231+
}
232+
}
233+
const enums = data.enums as SchemaEnum[];
234+
for (const e of enums) {
235+
e.kind = "enum";
236+
e.metadata ??= [];
237+
for (const m of (e.members ??= [])) {
238+
m.metadata ??= [];
239+
}
240+
}
225241

226-
assignDefaults(classes as SchemaClass[]);
242+
assignDefaults(classes);
227243

228244
// Sort all declarations by module then name, build map in one pass
229245
const all: Declaration[] = [...classes, ...enums, ...intrinsicDeclarations.values()];

src/entry.client.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { hydrateRoot } from "react-dom/client";
2+
import { HydratedRouter } from "react-router/dom";
13
import { buildAllGameContexts } from "./data/derived";
4+
import { loadGameSchemas } from "./data/loader";
25
import { GAME_LIST, type GameId } from "./games-list";
36

47
async function hydrate() {
5-
const { loadGameSchemas } = await import("./data/loader");
68
const loaded = new Map<GameId, Awaited<ReturnType<typeof loadGameSchemas>>>();
79
const errors = new Map<GameId, string>();
810
await Promise.all(
@@ -16,8 +18,6 @@ async function hydrate() {
1618
);
1719
buildAllGameContexts(loaded, errors);
1820

19-
const { hydrateRoot } = await import("react-dom/client");
20-
const { HydratedRouter } = await import("react-router/dom");
2121
hydrateRoot(document, <HydratedRouter />);
2222
}
2323

vite.config.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { readFileSync } from "node:fs";
2+
import { resolve } from "node:path";
13
import { reactRouter } from "@react-router/dev/vite";
24
import wyw from "@wyw-in-js/vite";
35
import { createLogger, defineConfig } from "vite";
@@ -31,6 +33,25 @@ export default defineConfig(({ mode }) => {
3133
presets: ["@babel/preset-typescript"],
3234
},
3335
}),
36+
{
37+
// sirv treats .gz files as pre-compressed and adds Content-Encoding: gzip,
38+
// causing the browser to auto-decompress. Serve them ourselves instead.
39+
name: "serve-gz-raw",
40+
configureServer(server) {
41+
server.middlewares.use((req, res, next) => {
42+
const match = req.url?.match(/\/schemas\/(\w+\.json\.gz)$/);
43+
if (!match) return next();
44+
try {
45+
const data = readFileSync(resolve("schemas", match[1]));
46+
res.setHeader("Content-Type", "application/gzip");
47+
res.setHeader("Content-Length", data.length);
48+
res.end(data);
49+
} catch {
50+
next();
51+
}
52+
});
53+
},
54+
},
3455
],
3556
build: {
3657
sourcemap: true,

0 commit comments

Comments
 (0)