Skip to content

Commit 4b2a50f

Browse files
committed
improve loading
1 parent e86af02 commit 4b2a50f

9 files changed

Lines changed: 91 additions & 98 deletions

File tree

web/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"devDependencies": {
2020
"@swc/plugin-styled-components": "^12.7.0",
21+
"@types/react": "^19.2.14",
2122
"@vitejs/plugin-react-swc": "^4.3.0",
2223
"oxfmt": "^0.40.0",
2324
"oxlint": "^1.55.0",

web/src/components/Docs/ContentList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function renderItem(declaration: Declaration) {
101101
}
102102

103103
export function ContentList() {
104-
const { declarations } = useContext(DeclarationsContext);
104+
const { declarations, loading } = useContext(DeclarationsContext);
105105
const { data, isSearching } = useFilteredData(declarations);
106106

107107
return (
@@ -116,7 +116,7 @@ export function ContentList() {
116116
<TextMessage>No results found</TextMessage>
117117
) : (
118118
<>
119-
<TextMessage>Choose a class or enum from the sidebar, or use search...</TextMessage>
119+
<TextMessage>{loading ? "Loading schemas..." : "Choose a class or enum from the sidebar, or use search..."}</TextMessage>
120120
<InfoBlock>
121121
<p>
122122
Source 2 includes a schema system that describes the engine's classes, fields, and

web/src/components/Docs/CrossGameRefs.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,20 @@ export function CrossGameRefs({
7070
module: string;
7171
kind: "class" | "enum";
7272
}) {
73-
const { game, allGames, declarations } = useContext(DeclarationsContext);
74-
if (!allGames) return null;
73+
const { game, declarations, otherGames } = useContext(DeclarationsContext);
7574

7675
const current = declarations.find(
7776
(d) => d.name === name && d.module === module && d.kind === kind,
7877
);
7978
if (!current) return null;
8079

81-
const otherGames: { gameId: GameId; gameName: string; status: DiffStatus; module: string }[] = [];
80+
const matches: { gameId: GameId; gameName: string; status: DiffStatus; module: string }[] = [];
8281

83-
for (const [gameId, gameDeclarations] of allGames) {
84-
if (gameId === game) continue;
82+
for (const [gameId, gameDeclarations] of otherGames) {
8583
const match = gameDeclarations.find((d) => d.name === name && d.kind === kind);
8684
if (match) {
8785
const gameInfo = GAMES.find((g) => g.id === gameId);
88-
otherGames.push({
86+
matches.push({
8987
gameId,
9088
gameName: gameInfo?.name ?? gameId,
9189
status: compareDeclarations(current, match),
@@ -94,13 +92,13 @@ export function CrossGameRefs({
9492
}
9593
}
9694

97-
if (otherGames.length === 0) return null;
95+
if (matches.length === 0) return null;
9896

9997
return (
10098
<SectionWrapper>
10199
<SectionTitle>Also in</SectionTitle>
102100
<SectionList>
103-
{otherGames.map(({ gameId, gameName, status, module: otherModule }) => {
101+
{matches.map(({ gameId, gameName, status, module: otherModule }) => {
104102
const gameInfo = GAMES.find((g) => g.id === gameId);
105103
return (
106104
<GameLink

web/src/components/Docs/DeclarationsContext.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ export type DeclarationsContextType = {
1414
root: string;
1515
declarations: Declaration[];
1616
references: Map<string, ReferenceEntry[]>;
17-
allGames: Map<GameId, Declaration[]> | null;
17+
otherGames: Map<GameId, Declaration[]>;
18+
loading: boolean;
1819
};
1920

2021
export const DeclarationsContext = createContext<DeclarationsContextType>({
2122
game: "cs2",
2223
root: "",
2324
declarations: [],
2425
references: new Map(),
25-
allGames: null,
26+
otherGames: new Map(),
27+
loading: false,
2628
});

web/src/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ function App() {
4242
<AppWrapper>
4343
<GlobalStyle />
4444
<HashRouter>
45-
<React.Suspense fallback={null}>
46-
<AppRoutes />
47-
</React.Suspense>
45+
<AppRoutes />
4846
</HashRouter>
4947
</AppWrapper>
5048
</ThemeProvider>

web/src/pages/data.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Declaration, SchemaClass, SchemaEnum } from "../components/Docs/api";
2-
import { GameId, GAMES } from "../games";
2+
import { GameId } from "../games";
33

44
interface SchemasJson {
55
classes: Omit<SchemaClass, "kind">[];
66
enums: Omit<SchemaEnum, "kind">[];
77
}
88

9-
const cache = new Map<GameId, Declaration[]>();
9+
const cache = new Map<GameId, Promise<Declaration[]>>();
1010

1111
function parseSchemas(data: SchemasJson): Declaration[] {
1212
const classes: Declaration[] = data.classes.map((c) => ({ ...c, kind: "class" as const }));
@@ -25,31 +25,30 @@ function parseSchemas(data: SchemasJson): Declaration[] {
2525
return all.sort((a, b) => a.name.localeCompare(b.name));
2626
}
2727

28-
export async function loadGameSchemas(gameId: GameId): Promise<Declaration[]> {
28+
export function loadGameSchemas(gameId: GameId): Promise<Declaration[]> {
2929
const cached = cache.get(gameId);
3030
if (cached) return cached;
3131

32-
const response = await fetch(`${import.meta.env.BASE_URL}${gameId}.json.gz`);
33-
if (!response.ok) throw new Error(`Failed to load ${gameId} schemas: ${response.status}`);
34-
35-
let data: SchemasJson;
36-
if (response.headers.get("Content-Type")?.includes("application/json")) {
37-
// Vite dev server: already decompressed via Content-Encoding
38-
data = await response.json();
39-
} else {
40-
// GitHub Pages / static hosting: raw gzip bytes
41-
const decompressed = response.body!.pipeThrough(new DecompressionStream("gzip"));
42-
data = await new Response(decompressed).json();
43-
}
32+
const promise = (async () => {
33+
const response = await fetch(`${import.meta.env.BASE_URL}${gameId}.json.gz`);
34+
if (!response.ok) throw new Error(`Failed to load ${gameId} schemas: ${response.status}`);
35+
36+
let data: SchemasJson;
37+
if (response.headers.get("Content-Type")?.includes("application/json")) {
38+
// Vite dev server: already decompressed via Content-Encoding
39+
data = await response.json();
40+
} else {
41+
// GitHub Pages / static hosting: raw gzip bytes
42+
const decompressed = response.body!.pipeThrough(new DecompressionStream("gzip"));
43+
data = await new Response(decompressed).json();
44+
}
4445

45-
const result = parseSchemas(data);
46-
cache.set(gameId, result);
47-
return result;
48-
}
46+
return parseSchemas(data);
47+
})().catch((e) => {
48+
cache.delete(gameId);
49+
throw e;
50+
});
4951

50-
export async function loadAllSchemas(): Promise<Map<GameId, Declaration[]>> {
51-
const entries = await Promise.all(
52-
GAMES.map(async (g) => [g.id, await loadGameSchemas(g.id)] as const),
53-
);
54-
return new Map(entries);
52+
cache.set(gameId, promise);
53+
return promise;
5554
}

web/src/pages/index.tsx

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
1-
import React from "react";
21
import { Navigate, Route, Routes } from "react-router-dom";
3-
import styled from "styled-components";
4-
import { NavBar } from "../components/layout/NavBar";
5-
6-
const SchemasPage = React.lazy(() => import("./schemas"));
7-
8-
const ErrorPage = styled.div`
9-
margin: auto;
10-
text-align: center;
11-
font-size: 24px;
12-
`;
2+
import SchemasPage from "./schemas";
133

144
export function AppRoutes() {
155
return (
166
<Routes>
17-
<Route path="/" element={<Navigate to="/cs2" replace />} />
187
<Route path="/:game/:module?/:scope?" element={<SchemasPage />} />
19-
<Route
20-
path="*"
21-
element={
22-
<>
23-
<NavBar />
24-
<ErrorPage>404 Not Found</ErrorPage>
25-
</>
26-
}
27-
/>
8+
<Route path="*" element={<Navigate to="/cs2" replace />} />
289
</Routes>
2910
);
3011
}

web/src/pages/schemas/index.tsx

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,70 @@
11
import React, { useEffect, useState } from "react";
2-
import { useParams } from "react-router-dom";
3-
import styled from "styled-components";
2+
import { Navigate, useParams } from "react-router-dom";
43
import { Declaration } from "../../components/Docs/api";
54
import { NavBar } from "../../components/layout/NavBar";
6-
import { isGameId, GameId } from "../../games";
7-
import { loadAllSchemas } from "../data";
5+
import { TextMessage } from "../../components/layout/Content";
6+
import { isGameId, GameId, GAMES } from "../../games";
7+
import { loadGameSchemas } from "../data";
88
import DeclarationsPage from "../DeclarationsPage";
99

10-
const LoadingMessage = styled.div`
11-
margin: auto;
12-
text-align: center;
13-
font-size: 24px;
14-
`;
10+
const EMPTY_DECLARATIONS: Declaration[] = [];
11+
const EMPTY_OTHER_GAMES = new Map<GameId, Declaration[]>();
1512

1613
export default function SchemasPage() {
1714
const { game } = useParams<{ game: string }>();
18-
const [allGames, setAllGames] = useState<Map<GameId, Declaration[]> | null>(null);
15+
const [declarations, setDeclarations] = useState<Declaration[] | null>(null);
16+
const [otherGames, setOtherGames] = useState<Map<GameId, Declaration[]>>(EMPTY_OTHER_GAMES);
1917
const [error, setError] = useState<string | null>(null);
2018
const validGame = game && isGameId(game) ? game : null;
2119

2220
useEffect(() => {
2321
if (!validGame) return;
24-
loadAllSchemas()
25-
.then(setAllGames)
26-
.catch((e) => setError(e instanceof Error ? e.message : String(e)));
22+
let stale = false;
23+
24+
setDeclarations(null);
25+
setOtherGames(EMPTY_OTHER_GAMES);
26+
setError(null);
27+
28+
loadGameSchemas(validGame)
29+
.then((data) => {
30+
if (stale) return;
31+
setDeclarations(data);
32+
33+
// Load other games in background for cross-game refs
34+
for (const g of GAMES) {
35+
if (g.id === validGame) continue;
36+
loadGameSchemas(g.id)
37+
.then((data) => { if (!stale) setOtherGames((prev) => new Map(prev).set(g.id, data)); })
38+
.catch(() => {});
39+
}
40+
})
41+
.catch((e) => { if (!stale) setError(e instanceof Error ? e.message : String(e)); });
42+
43+
return () => { stale = true; };
2744
}, [validGame]);
2845

2946
if (!validGame) {
30-
return (
31-
<>
32-
<NavBar />
33-
<LoadingMessage>Unknown game: {game}</LoadingMessage>
34-
</>
35-
);
47+
return <Navigate to="/cs2" replace />;
3648
}
3749

38-
if (error)
50+
if (error) {
3951
return (
4052
<>
4153
<NavBar />
42-
<LoadingMessage>Failed to load schemas: {error}</LoadingMessage>
43-
</>
44-
);
45-
if (!allGames)
46-
return (
47-
<>
48-
<NavBar />
49-
<LoadingMessage>Loading schemas...</LoadingMessage>
50-
</>
51-
);
52-
53-
const declarations = allGames.get(validGame);
54-
if (!declarations)
55-
return (
56-
<>
57-
<NavBar />
58-
<LoadingMessage>No data for {validGame}</LoadingMessage>
54+
<TextMessage>Failed to load schemas: {error}</TextMessage>
5955
</>
6056
);
57+
}
6158

6259
return (
6360
<DeclarationsPage
64-
context={{ game: validGame, root: `/${validGame}`, declarations, allGames }}
61+
context={{
62+
game: validGame,
63+
root: `/${validGame}`,
64+
declarations: declarations ?? EMPTY_DECLARATIONS,
65+
otherGames,
66+
loading: !declarations,
67+
}}
6568
/>
6669
);
6770
}

0 commit comments

Comments
 (0)