-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdata-loader.ts
More file actions
141 lines (128 loc) · 4.2 KB
/
data-loader.ts
File metadata and controls
141 lines (128 loc) · 4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* Fetch orchestration layer.
*
* This module owns all network I/O and parallel dispatch for game data JSON
* files. It fetches all required assets (data, locale, pinyin, mods) in
* parallel via Promise.allSettled, reports progress from the main all.json
* download, and swallows errors for optional assets (locale, pinyin, mods).
*/
import { DEFAULT_LOCALE, getDataJSONUrl } from "./constants";
import { HTTPError } from "./utils/http-errors";
import { retry } from "./utils/retry";
export type ProgressCallback = (
receivedBytes: number,
totalBytes: number | undefined,
) => void;
const noopProgress: ProgressCallback = () => {};
export type LoadedRawDataset = {
dataJSON: unknown;
localeJSON: unknown | undefined;
pinyinJSON: unknown | undefined;
modsJSON: unknown | undefined;
};
async function fetchJSONWithProgress<T>(
url: string,
progress: ProgressCallback = noopProgress,
): Promise<T> {
let response: Response;
try {
response = await fetch(url);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown fetch error";
throw new Error(`Failed to fetch ${url}: ${message}`);
}
if (!response.ok) {
throw new HTTPError(
`HTTP ${response.status} (${response.statusText}) fetching ${url}`,
response.status,
url,
);
}
try {
return (await response.json()) as T;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown parse error";
throw new Error(`Failed to parse JSON from ${url}: ${message}`);
} finally {
progress(100, 100);
}
}
/**
* Fetches the main game data blob (`all.json`) for a given build version.
*
* @param version Build version slug (e.g. "nightly", "v0.9.1")
* @param progress Callback invoked with (receivedBytes, totalBytes) during download
* @returns Raw parsed JSON with the dataset payload and build number.
*/
async function fetchRawData<T>(
version: string,
progress: ProgressCallback,
): Promise<T> {
return await retry(() =>
fetchJSONWithProgress(getDataJSONUrl(version, "all.json"), progress),
);
}
/**
* Fetches a locale PO/MO translation file for a given build version and locale.
*
* @param version Build version slug
* @param locale Locale code (e.g. "uk", "zh_CN")
* @returns Raw parsed locale JSON blob
*/
function fetchRawLocale<T>(version: string, locale: string): Promise<T> {
return retry(
() => fetchJSONWithProgress(getDataJSONUrl(version, `lang/${locale}.json`)),
{ maxAttempts: 2, baseDelayMs: 1000 },
);
}
/**
* Fetches the mod index (`all_mods.json`) for a given build version.
*
* @param version Build version slug
* @returns Raw parsed mod JSON blob keyed by mod id
*/
async function fetchRawMods<T>(version: string): Promise<T> {
return retry(
() => fetchJSONWithProgress(getDataJSONUrl(version, "all_mods.json")),
{ maxAttempts: 2, baseDelayMs: 1000 },
);
}
export async function loadRawDataset(
version: string,
locale: string,
onProgress: ProgressCallback,
): Promise<LoadedRawDataset> {
const needsLocale = locale !== DEFAULT_LOCALE;
const needsPinyin = locale.startsWith("zh_");
const [dataResult, localeResult, pinyinResult, modsResult] =
await Promise.allSettled([
fetchRawData(version, onProgress),
needsLocale
? fetchRawLocale(version, locale)
: Promise.resolve(undefined),
needsPinyin
? fetchRawLocale(version, `${locale}_pinyin`)
: Promise.resolve(undefined),
fetchRawMods(version),
]);
if (dataResult.status === "rejected") {
throw dataResult.reason;
}
if (localeResult.status === "rejected") {
console.warn(`Failed to load locale ${locale}:`, localeResult.reason);
}
if (pinyinResult.status === "rejected") {
console.warn(`Failed to load pinyin for ${locale}:`, pinyinResult.reason);
}
if (modsResult.status === "rejected") {
console.warn("Failed to load mods catalog:", modsResult.reason);
}
return {
dataJSON: dataResult.value,
localeJSON:
localeResult.status === "fulfilled" ? localeResult.value : undefined,
pinyinJSON:
pinyinResult.status === "fulfilled" ? pinyinResult.value : undefined,
modsJSON: modsResult.status === "fulfilled" ? modsResult.value : undefined,
};
}