@rhyster/wow-casc-dbc 1.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ const { buffer } = await client.getFileByContentKey(cKey.cKey);
24
24
 
25
25
  // Parse DB2 file
26
26
  const reader = new WDCReader(buffer);
27
- const parser = new DBDParser(reader);
27
+ const parser = await DBDParser.parse(reader);
28
28
 
29
29
  // Access DB2 file
30
30
  // reader.getRowData
package/dist/index.cjs CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  const assert = require('node:assert');
4
4
  const crypto = require('node:crypto');
5
+ const async = require('async');
6
+ const cliProgress = require('cli-progress');
5
7
  const fs = require('node:fs/promises');
6
8
  const path = require('node:path');
7
9
  const http = require('node:http');
@@ -11,6 +13,7 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
11
13
 
12
14
  const assert__default = /*#__PURE__*/_interopDefaultCompat(assert);
13
15
  const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
16
+ const cliProgress__default = /*#__PURE__*/_interopDefaultCompat(cliProgress);
14
17
  const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
15
18
  const path__default = /*#__PURE__*/_interopDefaultCompat(path);
16
19
  const http__default = /*#__PURE__*/_interopDefaultCompat(http);
@@ -60,7 +63,11 @@ const CACHE_DIRS = {
60
63
  const CACHE_INTEGRITY_FILE = path__default.resolve(CACHE_ROOT, "integrity.json");
61
64
  const cacheIntegrity = new Store(CACHE_INTEGRITY_FILE);
62
65
  const formatCDNKey = (key) => `${key.substring(0, 2)}/${key.substring(2, 4)}/${key}`;
63
- const requestData = async (url, partialOffset = void 0, partialLength = void 0) => new Promise((resolve, reject) => {
66
+ const requestData = async (url, {
67
+ partialOffset,
68
+ partialLength,
69
+ showProgress
70
+ } = {}) => new Promise((resolve, reject) => {
64
71
  const options = {
65
72
  headers: {
66
73
  "User-Agent": USER_AGENT,
@@ -70,7 +77,9 @@ const requestData = async (url, partialOffset = void 0, partialLength = void 0)
70
77
  http__default.get(url, options, (res) => {
71
78
  if (res.statusCode === 301 || res.statusCode === 302) {
72
79
  if (res.headers.location) {
73
- requestData(res.headers.location, partialOffset, partialLength).then(resolve).catch(reject);
80
+ requestData(res.headers.location, { partialOffset, partialLength, showProgress }).then(resolve).catch((err) => {
81
+ throw err;
82
+ });
74
83
  } else {
75
84
  reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode.toString()}`));
76
85
  }
@@ -80,17 +89,39 @@ const requestData = async (url, partialOffset = void 0, partialLength = void 0)
80
89
  reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode?.toString() ?? "undefined"}`));
81
90
  return;
82
91
  }
92
+ const lengthText = res.headers["content-length"];
93
+ const length = lengthText ? parseInt(lengthText, 10) : 0;
94
+ const bar = showProgress && !Number.isNaN(length) && length >= 10485760 ? new cliProgress__default.SingleBar({ etaBuffer: 10240 }, cliProgress__default.Presets.shades_classic) : void 0;
95
+ bar?.start(length, 0);
83
96
  const chunks = [];
84
- res.on("data", (chunk) => chunks.push(chunk));
97
+ res.on("data", (chunk) => {
98
+ bar?.increment(chunk.length);
99
+ chunks.push(chunk);
100
+ });
85
101
  res.on("end", () => {
102
+ bar?.stop();
86
103
  resolve(Buffer.concat(chunks));
87
104
  });
105
+ res.on("error", (err) => {
106
+ bar?.stop();
107
+ reject(err);
108
+ });
88
109
  }).on("error", reject).end();
89
110
  });
90
- const downloadFile = (prefixes, type, key, partialOffset = void 0, partialLength = void 0) => {
111
+ const downloadFile = (prefixes, type, key, {
112
+ partialOffset,
113
+ partialLength,
114
+ showProgress,
115
+ showAttemptFail
116
+ } = {}) => {
91
117
  const urls = prefixes.map((prefix) => `${prefix}/${type}/${formatCDNKey(key)}`);
92
118
  return urls.reduce(
93
- (prev, url) => prev.catch(() => requestData(url, partialOffset, partialLength)),
119
+ (prev, url, index) => prev.catch((err) => {
120
+ if (showAttemptFail && index > 0 && err instanceof Error) {
121
+ console.warn(`${( new Date()).toISOString()} [WARN]:`, err.message);
122
+ }
123
+ return requestData(url, { partialOffset, partialLength, showProgress });
124
+ }),
94
125
  Promise.reject(new Error(""))
95
126
  );
96
127
  };
@@ -108,7 +139,13 @@ const getFileCache = async (file) => {
108
139
  }
109
140
  return void 0;
110
141
  };
111
- const getDataFile = async (prefixes, key, type, buildCKey, name, partialOffset = void 0, partialLength = void 0) => {
142
+ const getDataFile = async (prefixes, key, type, buildCKey, {
143
+ name,
144
+ partialOffset,
145
+ partialLength,
146
+ showProgress,
147
+ showAttemptFail
148
+ } = {}) => {
112
149
  const dir = type === "build" ? path__default.join(CACHE_DIRS[type], buildCKey) : CACHE_DIRS[type];
113
150
  const file = name ? path__default.join(dir, name) : path__default.join(dir, key);
114
151
  const cacheBuffer = await getFileCache(file);
@@ -118,7 +155,12 @@ const getDataFile = async (prefixes, key, type, buildCKey, name, partialOffset =
118
155
  }
119
156
  return cacheBuffer;
120
157
  }
121
- const downloadBuffer = await downloadFile(prefixes, "data", key, partialOffset, partialLength);
158
+ const downloadBuffer = await downloadFile(prefixes, "data", key, {
159
+ partialOffset,
160
+ partialLength,
161
+ showProgress,
162
+ showAttemptFail
163
+ });
122
164
  if (partialOffset === void 0 && partialLength === void 0 || name) {
123
165
  await fs__default.mkdir(path__default.resolve(CACHE_ROOT, dir), { recursive: true });
124
166
  await fs__default.writeFile(path__default.resolve(CACHE_ROOT, file), downloadBuffer);
@@ -127,8 +169,11 @@ const getDataFile = async (prefixes, key, type, buildCKey, name, partialOffset =
127
169
  }
128
170
  return downloadBuffer;
129
171
  };
130
- const getConfigFile = async (prefixes, key) => {
131
- const downloadBuffer = await downloadFile(prefixes, "config", key);
172
+ const getConfigFile = async (prefixes, key, {
173
+ showProgress,
174
+ showAttemptFail
175
+ } = {}) => {
176
+ const downloadBuffer = await downloadFile(prefixes, "config", key, { showProgress, showAttemptFail });
132
177
  return downloadBuffer.toString("utf-8");
133
178
  };
134
179
  const getProductVersions = async (region, product) => {
@@ -1329,56 +1374,6 @@ const resolveCDNHost = async (hosts, path) => {
1329
1374
  const resolved = latencies.filter((result) => result.status === "fulfilled").map((result) => result.value).sort((a, b) => a.latency - b.latency);
1330
1375
  return resolved.map((result) => `http://${result.host}/${path}`);
1331
1376
  };
1332
- const startProcessBar = (total, screenWidth = 80) => {
1333
- const totalText = total.toString();
1334
- const barLength = screenWidth - totalText.length * 2 - 4;
1335
- const bar = " ".repeat(barLength);
1336
- process.stdout.write(`[${bar}] ${"0".padStart(totalText.length, " ")}/${totalText}`);
1337
- process.stdout.cursorTo(0);
1338
- };
1339
- const updateProcessBar = (current, total, screenWidth = 80) => {
1340
- const totalText = total.toString();
1341
- const barLength = screenWidth - totalText.length * 2 - 4;
1342
- const bar = "=".repeat(Math.floor(current / total * barLength));
1343
- process.stdout.write(`[${bar.padEnd(barLength, " ")}] ${current.toString().padStart(totalText.length, " ")}/${totalText}`);
1344
- process.stdout.cursorTo(0);
1345
- };
1346
- const endProcessBar = () => {
1347
- process.stdout.write("\n");
1348
- };
1349
- const asyncQueue = (items, handler, limit) => {
1350
- if (items.length === 0) {
1351
- return Promise.resolve([]);
1352
- }
1353
- return new Promise((resolve, reject) => {
1354
- const results = [];
1355
- let current = 0;
1356
- let pending = 0;
1357
- let completed = 0;
1358
- const next = () => {
1359
- if (current < items.length) {
1360
- const index = current;
1361
- current += 1;
1362
- pending += 1;
1363
- handler(items[index]).then((result) => {
1364
- results[index] = result;
1365
- }).catch(reject).finally(() => {
1366
- pending -= 1;
1367
- completed += 1;
1368
- updateProcessBar(completed, items.length);
1369
- next();
1370
- });
1371
- } else if (pending === 0) {
1372
- endProcessBar();
1373
- resolve(results);
1374
- }
1375
- };
1376
- startProcessBar(items.length);
1377
- for (let i = 0; i < limit; i += 1) {
1378
- next();
1379
- }
1380
- });
1381
- };
1382
1377
  const JEDEC = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
1383
1378
  const formatFileSize = (input) => {
1384
1379
  if (Number.isNaN(input))
@@ -1445,7 +1440,7 @@ class CASCClient {
1445
1440
  }
1446
1441
  log(level, message) {
1447
1442
  if (level <= this.logLevel) {
1448
- if (level >= 0 /* error */) {
1443
+ if (level <= 0 /* error */) {
1449
1444
  console.error(`${( new Date()).toISOString()} [${textLogLevel[level]}]:`, message);
1450
1445
  } else {
1451
1446
  console.log(`${( new Date()).toISOString()} [${textLogLevel[level]}]:`, message);
@@ -1471,32 +1466,55 @@ class CASCClient {
1471
1466
  this.log(2 /* info */, prefix);
1472
1467
  });
1473
1468
  this.log(2 /* info */, "Fetching build configurations...");
1474
- const cdnConfigText = await getConfigFile(prefixes, this.version.CDNConfig);
1469
+ const cdnConfigText = await getConfigFile(prefixes, this.version.CDNConfig, {
1470
+ showAttemptFail: this.logLevel >= 1 /* warn */
1471
+ });
1475
1472
  const cdnConfig = parseCDNConfig(cdnConfigText);
1476
- const buildConfigText = await getConfigFile(prefixes, this.version.BuildConfig);
1473
+ const buildConfigText = await getConfigFile(prefixes, this.version.BuildConfig, {
1474
+ showAttemptFail: this.logLevel >= 1 /* warn */
1475
+ });
1477
1476
  const buildConfig = parseBuildConfig(buildConfigText);
1478
1477
  this.log(2 /* info */, "Loading archives...");
1479
1478
  const archiveKeys = cdnConfig.archives.split(" ");
1480
1479
  const archiveCount = archiveKeys.length;
1481
1480
  const archiveTotalSize = cdnConfig.archivesIndexSize.split(" ").reduce((a, b) => a + parseInt(b, 10), 0);
1482
- const archives = new Map(
1483
- (await asyncQueue(
1484
- archiveKeys,
1485
- async (key) => {
1486
- const fileName = `${key}.index`;
1487
- const buffer = await getDataFile(prefixes, fileName, "indexes", this.version.BuildConfig);
1488
- return parseArchiveIndex(buffer, key);
1489
- },
1490
- 50
1491
- )).flatMap((e) => [...e])
1492
- );
1481
+ const archiveBar = this.logLevel >= 2 /* info */ ? new cliProgress__default.SingleBar({ etaBuffer: 100 }, cliProgress__default.Presets.shades_classic) : void 0;
1482
+ archiveBar?.start(archiveCount, 0);
1483
+ const archivesMapArray = await async.mapLimit(
1484
+ archiveKeys,
1485
+ 50,
1486
+ async (key) => {
1487
+ const fileName = `${key}.index`;
1488
+ const buffer = await async.retry({
1489
+ times: 5,
1490
+ interval: 3e3
1491
+ }, async () => getDataFile(prefixes, fileName, "indexes", this.version.BuildConfig, {
1492
+ showProgress: this.logLevel >= 2 /* info */,
1493
+ showAttemptFail: this.logLevel >= 1 /* warn */
1494
+ }));
1495
+ const map = parseArchiveIndex(buffer, key);
1496
+ archiveBar?.increment();
1497
+ return map;
1498
+ }
1499
+ ).then((result) => {
1500
+ archiveBar?.stop();
1501
+ return result.flatMap((e) => [...e]);
1502
+ }).catch((error) => {
1503
+ archiveBar?.stop();
1504
+ throw error;
1505
+ });
1506
+ const archives = new Map(archivesMapArray);
1493
1507
  this.log(
1494
1508
  2 /* info */,
1495
1509
  `Loaded ${archiveCount.toString()} archives (${archives.size.toString()} entries, ${formatFileSize(archiveTotalSize)})`
1496
1510
  );
1497
1511
  this.log(2 /* info */, "Loading encoding table...");
1498
1512
  const [encodingCKey, encodingEKey] = buildConfig.encoding.split(" ");
1499
- const encodingBuffer = await getDataFile(prefixes, encodingEKey, "build", this.version.BuildConfig, "encoding");
1513
+ const encodingBuffer = await getDataFile(prefixes, encodingEKey, "build", this.version.BuildConfig, {
1514
+ name: "encoding",
1515
+ showProgress: this.logLevel >= 2 /* info */,
1516
+ showAttemptFail: this.logLevel >= 1 /* warn */
1517
+ });
1500
1518
  this.log(2 /* info */, `Loaded encoding table (${formatFileSize(encodingBuffer.byteLength)})`);
1501
1519
  this.log(2 /* info */, "Parsing encoding table...");
1502
1520
  const encoding = parseEncodingFile(encodingBuffer, encodingEKey, encodingCKey);
@@ -1506,7 +1524,11 @@ class CASCClient {
1506
1524
  const rootEKeys = encoding.cKey2EKey.get(rootCKey);
1507
1525
  assert__default(rootEKeys, "Failing to find EKey for root table.");
1508
1526
  const rootEKey = typeof rootEKeys === "string" ? rootEKeys : rootEKeys[0];
1509
- const rootBuffer = await getDataFile(prefixes, rootEKey, "build", this.version.BuildConfig, "root");
1527
+ const rootBuffer = await getDataFile(prefixes, rootEKey, "build", this.version.BuildConfig, {
1528
+ name: "root",
1529
+ showProgress: this.logLevel >= 2 /* info */,
1530
+ showAttemptFail: this.logLevel >= 1 /* warn */
1531
+ });
1510
1532
  this.log(2 /* info */, `Loaded root table (${formatFileSize(rootBuffer.byteLength)})`);
1511
1533
  this.log(2 /* info */, "Parsing root file...");
1512
1534
  const rootFile = parseRootFile(rootBuffer, rootEKey, rootCKey);
@@ -1585,7 +1607,16 @@ class CASCClient {
1585
1607
  assert__default(eKeys, `Failing to find encoding key for ${cKey}`);
1586
1608
  const eKey = typeof eKeys === "string" ? eKeys : eKeys[0];
1587
1609
  const archive = archives.get(eKey);
1588
- const blte = archive ? await getDataFile(prefixes, archive.key, "data", this.version.BuildConfig, eKey, archive.offset, archive.size) : await getDataFile(prefixes, eKey, "data", this.version.BuildConfig);
1610
+ const blte = archive ? await getDataFile(prefixes, archive.key, "data", this.version.BuildConfig, {
1611
+ name: eKey,
1612
+ partialOffset: archive.offset,
1613
+ partialLength: archive.size,
1614
+ showProgress: this.logLevel >= 2 /* info */,
1615
+ showAttemptFail: this.logLevel >= 1 /* warn */
1616
+ }) : await getDataFile(prefixes, eKey, "data", this.version.BuildConfig, {
1617
+ showProgress: this.logLevel >= 2 /* info */,
1618
+ showAttemptFail: this.logLevel >= 1 /* warn */
1619
+ });
1589
1620
  const reader = new BLTEReader(blte, eKey, this.keys);
1590
1621
  if (!allowMissingKey) {
1591
1622
  reader.processBytes(allowMissingKey);
@@ -1721,6 +1752,11 @@ class DBDParser {
1721
1752
  }
1722
1753
  });
1723
1754
  }
1755
+ static async parse(wdc) {
1756
+ const parser = new DBDParser(wdc);
1757
+ await parser.init();
1758
+ return parser;
1759
+ }
1724
1760
  getAllIDs() {
1725
1761
  return this.wdc.getAllIDs();
1726
1762
  }
package/dist/index.d.cts CHANGED
@@ -224,8 +224,9 @@ declare class DBDParser {
224
224
  readonly wdc: WDCReader;
225
225
  readonly definitions: Map<string, string>;
226
226
  columns: Column[];
227
- constructor(wdc: WDCReader);
228
- init(): Promise<void>;
227
+ private constructor();
228
+ private init;
229
+ static parse(wdc: WDCReader): Promise<DBDParser>;
229
230
  getAllIDs(): number[];
230
231
  getRowData(id: number): Record<string, ColumnData | ColumnData[]> | undefined;
231
232
  }
package/dist/index.d.mts CHANGED
@@ -224,8 +224,9 @@ declare class DBDParser {
224
224
  readonly wdc: WDCReader;
225
225
  readonly definitions: Map<string, string>;
226
226
  columns: Column[];
227
- constructor(wdc: WDCReader);
228
- init(): Promise<void>;
227
+ private constructor();
228
+ private init;
229
+ static parse(wdc: WDCReader): Promise<DBDParser>;
229
230
  getAllIDs(): number[];
230
231
  getRowData(id: number): Record<string, ColumnData | ColumnData[]> | undefined;
231
232
  }
package/dist/index.d.ts CHANGED
@@ -224,8 +224,9 @@ declare class DBDParser {
224
224
  readonly wdc: WDCReader;
225
225
  readonly definitions: Map<string, string>;
226
226
  columns: Column[];
227
- constructor(wdc: WDCReader);
228
- init(): Promise<void>;
227
+ private constructor();
228
+ private init;
229
+ static parse(wdc: WDCReader): Promise<DBDParser>;
229
230
  getAllIDs(): number[];
230
231
  getRowData(id: number): Record<string, ColumnData | ColumnData[]> | undefined;
231
232
  }
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  import assert from 'node:assert';
2
2
  import crypto from 'node:crypto';
3
+ import { mapLimit, retry } from 'async';
4
+ import cliProgress from 'cli-progress';
3
5
  import fs from 'node:fs/promises';
4
6
  import path from 'node:path';
5
7
  import http from 'node:http';
@@ -49,7 +51,11 @@ const CACHE_DIRS = {
49
51
  const CACHE_INTEGRITY_FILE = path.resolve(CACHE_ROOT, "integrity.json");
50
52
  const cacheIntegrity = new Store(CACHE_INTEGRITY_FILE);
51
53
  const formatCDNKey = (key) => `${key.substring(0, 2)}/${key.substring(2, 4)}/${key}`;
52
- const requestData = async (url, partialOffset = void 0, partialLength = void 0) => new Promise((resolve, reject) => {
54
+ const requestData = async (url, {
55
+ partialOffset,
56
+ partialLength,
57
+ showProgress
58
+ } = {}) => new Promise((resolve, reject) => {
53
59
  const options = {
54
60
  headers: {
55
61
  "User-Agent": USER_AGENT,
@@ -59,7 +65,9 @@ const requestData = async (url, partialOffset = void 0, partialLength = void 0)
59
65
  http.get(url, options, (res) => {
60
66
  if (res.statusCode === 301 || res.statusCode === 302) {
61
67
  if (res.headers.location) {
62
- requestData(res.headers.location, partialOffset, partialLength).then(resolve).catch(reject);
68
+ requestData(res.headers.location, { partialOffset, partialLength, showProgress }).then(resolve).catch((err) => {
69
+ throw err;
70
+ });
63
71
  } else {
64
72
  reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode.toString()}`));
65
73
  }
@@ -69,17 +77,39 @@ const requestData = async (url, partialOffset = void 0, partialLength = void 0)
69
77
  reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode?.toString() ?? "undefined"}`));
70
78
  return;
71
79
  }
80
+ const lengthText = res.headers["content-length"];
81
+ const length = lengthText ? parseInt(lengthText, 10) : 0;
82
+ const bar = showProgress && !Number.isNaN(length) && length >= 10485760 ? new cliProgress.SingleBar({ etaBuffer: 10240 }, cliProgress.Presets.shades_classic) : void 0;
83
+ bar?.start(length, 0);
72
84
  const chunks = [];
73
- res.on("data", (chunk) => chunks.push(chunk));
85
+ res.on("data", (chunk) => {
86
+ bar?.increment(chunk.length);
87
+ chunks.push(chunk);
88
+ });
74
89
  res.on("end", () => {
90
+ bar?.stop();
75
91
  resolve(Buffer.concat(chunks));
76
92
  });
93
+ res.on("error", (err) => {
94
+ bar?.stop();
95
+ reject(err);
96
+ });
77
97
  }).on("error", reject).end();
78
98
  });
79
- const downloadFile = (prefixes, type, key, partialOffset = void 0, partialLength = void 0) => {
99
+ const downloadFile = (prefixes, type, key, {
100
+ partialOffset,
101
+ partialLength,
102
+ showProgress,
103
+ showAttemptFail
104
+ } = {}) => {
80
105
  const urls = prefixes.map((prefix) => `${prefix}/${type}/${formatCDNKey(key)}`);
81
106
  return urls.reduce(
82
- (prev, url) => prev.catch(() => requestData(url, partialOffset, partialLength)),
107
+ (prev, url, index) => prev.catch((err) => {
108
+ if (showAttemptFail && index > 0 && err instanceof Error) {
109
+ console.warn(`${( new Date()).toISOString()} [WARN]:`, err.message);
110
+ }
111
+ return requestData(url, { partialOffset, partialLength, showProgress });
112
+ }),
83
113
  Promise.reject(new Error(""))
84
114
  );
85
115
  };
@@ -97,7 +127,13 @@ const getFileCache = async (file) => {
97
127
  }
98
128
  return void 0;
99
129
  };
100
- const getDataFile = async (prefixes, key, type, buildCKey, name, partialOffset = void 0, partialLength = void 0) => {
130
+ const getDataFile = async (prefixes, key, type, buildCKey, {
131
+ name,
132
+ partialOffset,
133
+ partialLength,
134
+ showProgress,
135
+ showAttemptFail
136
+ } = {}) => {
101
137
  const dir = type === "build" ? path.join(CACHE_DIRS[type], buildCKey) : CACHE_DIRS[type];
102
138
  const file = name ? path.join(dir, name) : path.join(dir, key);
103
139
  const cacheBuffer = await getFileCache(file);
@@ -107,7 +143,12 @@ const getDataFile = async (prefixes, key, type, buildCKey, name, partialOffset =
107
143
  }
108
144
  return cacheBuffer;
109
145
  }
110
- const downloadBuffer = await downloadFile(prefixes, "data", key, partialOffset, partialLength);
146
+ const downloadBuffer = await downloadFile(prefixes, "data", key, {
147
+ partialOffset,
148
+ partialLength,
149
+ showProgress,
150
+ showAttemptFail
151
+ });
111
152
  if (partialOffset === void 0 && partialLength === void 0 || name) {
112
153
  await fs.mkdir(path.resolve(CACHE_ROOT, dir), { recursive: true });
113
154
  await fs.writeFile(path.resolve(CACHE_ROOT, file), downloadBuffer);
@@ -116,8 +157,11 @@ const getDataFile = async (prefixes, key, type, buildCKey, name, partialOffset =
116
157
  }
117
158
  return downloadBuffer;
118
159
  };
119
- const getConfigFile = async (prefixes, key) => {
120
- const downloadBuffer = await downloadFile(prefixes, "config", key);
160
+ const getConfigFile = async (prefixes, key, {
161
+ showProgress,
162
+ showAttemptFail
163
+ } = {}) => {
164
+ const downloadBuffer = await downloadFile(prefixes, "config", key, { showProgress, showAttemptFail });
121
165
  return downloadBuffer.toString("utf-8");
122
166
  };
123
167
  const getProductVersions = async (region, product) => {
@@ -1318,56 +1362,6 @@ const resolveCDNHost = async (hosts, path) => {
1318
1362
  const resolved = latencies.filter((result) => result.status === "fulfilled").map((result) => result.value).sort((a, b) => a.latency - b.latency);
1319
1363
  return resolved.map((result) => `http://${result.host}/${path}`);
1320
1364
  };
1321
- const startProcessBar = (total, screenWidth = 80) => {
1322
- const totalText = total.toString();
1323
- const barLength = screenWidth - totalText.length * 2 - 4;
1324
- const bar = " ".repeat(barLength);
1325
- process.stdout.write(`[${bar}] ${"0".padStart(totalText.length, " ")}/${totalText}`);
1326
- process.stdout.cursorTo(0);
1327
- };
1328
- const updateProcessBar = (current, total, screenWidth = 80) => {
1329
- const totalText = total.toString();
1330
- const barLength = screenWidth - totalText.length * 2 - 4;
1331
- const bar = "=".repeat(Math.floor(current / total * barLength));
1332
- process.stdout.write(`[${bar.padEnd(barLength, " ")}] ${current.toString().padStart(totalText.length, " ")}/${totalText}`);
1333
- process.stdout.cursorTo(0);
1334
- };
1335
- const endProcessBar = () => {
1336
- process.stdout.write("\n");
1337
- };
1338
- const asyncQueue = (items, handler, limit) => {
1339
- if (items.length === 0) {
1340
- return Promise.resolve([]);
1341
- }
1342
- return new Promise((resolve, reject) => {
1343
- const results = [];
1344
- let current = 0;
1345
- let pending = 0;
1346
- let completed = 0;
1347
- const next = () => {
1348
- if (current < items.length) {
1349
- const index = current;
1350
- current += 1;
1351
- pending += 1;
1352
- handler(items[index]).then((result) => {
1353
- results[index] = result;
1354
- }).catch(reject).finally(() => {
1355
- pending -= 1;
1356
- completed += 1;
1357
- updateProcessBar(completed, items.length);
1358
- next();
1359
- });
1360
- } else if (pending === 0) {
1361
- endProcessBar();
1362
- resolve(results);
1363
- }
1364
- };
1365
- startProcessBar(items.length);
1366
- for (let i = 0; i < limit; i += 1) {
1367
- next();
1368
- }
1369
- });
1370
- };
1371
1365
  const JEDEC = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
1372
1366
  const formatFileSize = (input) => {
1373
1367
  if (Number.isNaN(input))
@@ -1434,7 +1428,7 @@ class CASCClient {
1434
1428
  }
1435
1429
  log(level, message) {
1436
1430
  if (level <= this.logLevel) {
1437
- if (level >= 0 /* error */) {
1431
+ if (level <= 0 /* error */) {
1438
1432
  console.error(`${( new Date()).toISOString()} [${textLogLevel[level]}]:`, message);
1439
1433
  } else {
1440
1434
  console.log(`${( new Date()).toISOString()} [${textLogLevel[level]}]:`, message);
@@ -1460,32 +1454,55 @@ class CASCClient {
1460
1454
  this.log(2 /* info */, prefix);
1461
1455
  });
1462
1456
  this.log(2 /* info */, "Fetching build configurations...");
1463
- const cdnConfigText = await getConfigFile(prefixes, this.version.CDNConfig);
1457
+ const cdnConfigText = await getConfigFile(prefixes, this.version.CDNConfig, {
1458
+ showAttemptFail: this.logLevel >= 1 /* warn */
1459
+ });
1464
1460
  const cdnConfig = parseCDNConfig(cdnConfigText);
1465
- const buildConfigText = await getConfigFile(prefixes, this.version.BuildConfig);
1461
+ const buildConfigText = await getConfigFile(prefixes, this.version.BuildConfig, {
1462
+ showAttemptFail: this.logLevel >= 1 /* warn */
1463
+ });
1466
1464
  const buildConfig = parseBuildConfig(buildConfigText);
1467
1465
  this.log(2 /* info */, "Loading archives...");
1468
1466
  const archiveKeys = cdnConfig.archives.split(" ");
1469
1467
  const archiveCount = archiveKeys.length;
1470
1468
  const archiveTotalSize = cdnConfig.archivesIndexSize.split(" ").reduce((a, b) => a + parseInt(b, 10), 0);
1471
- const archives = new Map(
1472
- (await asyncQueue(
1473
- archiveKeys,
1474
- async (key) => {
1475
- const fileName = `${key}.index`;
1476
- const buffer = await getDataFile(prefixes, fileName, "indexes", this.version.BuildConfig);
1477
- return parseArchiveIndex(buffer, key);
1478
- },
1479
- 50
1480
- )).flatMap((e) => [...e])
1481
- );
1469
+ const archiveBar = this.logLevel >= 2 /* info */ ? new cliProgress.SingleBar({ etaBuffer: 100 }, cliProgress.Presets.shades_classic) : void 0;
1470
+ archiveBar?.start(archiveCount, 0);
1471
+ const archivesMapArray = await mapLimit(
1472
+ archiveKeys,
1473
+ 50,
1474
+ async (key) => {
1475
+ const fileName = `${key}.index`;
1476
+ const buffer = await retry({
1477
+ times: 5,
1478
+ interval: 3e3
1479
+ }, async () => getDataFile(prefixes, fileName, "indexes", this.version.BuildConfig, {
1480
+ showProgress: this.logLevel >= 2 /* info */,
1481
+ showAttemptFail: this.logLevel >= 1 /* warn */
1482
+ }));
1483
+ const map = parseArchiveIndex(buffer, key);
1484
+ archiveBar?.increment();
1485
+ return map;
1486
+ }
1487
+ ).then((result) => {
1488
+ archiveBar?.stop();
1489
+ return result.flatMap((e) => [...e]);
1490
+ }).catch((error) => {
1491
+ archiveBar?.stop();
1492
+ throw error;
1493
+ });
1494
+ const archives = new Map(archivesMapArray);
1482
1495
  this.log(
1483
1496
  2 /* info */,
1484
1497
  `Loaded ${archiveCount.toString()} archives (${archives.size.toString()} entries, ${formatFileSize(archiveTotalSize)})`
1485
1498
  );
1486
1499
  this.log(2 /* info */, "Loading encoding table...");
1487
1500
  const [encodingCKey, encodingEKey] = buildConfig.encoding.split(" ");
1488
- const encodingBuffer = await getDataFile(prefixes, encodingEKey, "build", this.version.BuildConfig, "encoding");
1501
+ const encodingBuffer = await getDataFile(prefixes, encodingEKey, "build", this.version.BuildConfig, {
1502
+ name: "encoding",
1503
+ showProgress: this.logLevel >= 2 /* info */,
1504
+ showAttemptFail: this.logLevel >= 1 /* warn */
1505
+ });
1489
1506
  this.log(2 /* info */, `Loaded encoding table (${formatFileSize(encodingBuffer.byteLength)})`);
1490
1507
  this.log(2 /* info */, "Parsing encoding table...");
1491
1508
  const encoding = parseEncodingFile(encodingBuffer, encodingEKey, encodingCKey);
@@ -1495,7 +1512,11 @@ class CASCClient {
1495
1512
  const rootEKeys = encoding.cKey2EKey.get(rootCKey);
1496
1513
  assert(rootEKeys, "Failing to find EKey for root table.");
1497
1514
  const rootEKey = typeof rootEKeys === "string" ? rootEKeys : rootEKeys[0];
1498
- const rootBuffer = await getDataFile(prefixes, rootEKey, "build", this.version.BuildConfig, "root");
1515
+ const rootBuffer = await getDataFile(prefixes, rootEKey, "build", this.version.BuildConfig, {
1516
+ name: "root",
1517
+ showProgress: this.logLevel >= 2 /* info */,
1518
+ showAttemptFail: this.logLevel >= 1 /* warn */
1519
+ });
1499
1520
  this.log(2 /* info */, `Loaded root table (${formatFileSize(rootBuffer.byteLength)})`);
1500
1521
  this.log(2 /* info */, "Parsing root file...");
1501
1522
  const rootFile = parseRootFile(rootBuffer, rootEKey, rootCKey);
@@ -1574,7 +1595,16 @@ class CASCClient {
1574
1595
  assert(eKeys, `Failing to find encoding key for ${cKey}`);
1575
1596
  const eKey = typeof eKeys === "string" ? eKeys : eKeys[0];
1576
1597
  const archive = archives.get(eKey);
1577
- const blte = archive ? await getDataFile(prefixes, archive.key, "data", this.version.BuildConfig, eKey, archive.offset, archive.size) : await getDataFile(prefixes, eKey, "data", this.version.BuildConfig);
1598
+ const blte = archive ? await getDataFile(prefixes, archive.key, "data", this.version.BuildConfig, {
1599
+ name: eKey,
1600
+ partialOffset: archive.offset,
1601
+ partialLength: archive.size,
1602
+ showProgress: this.logLevel >= 2 /* info */,
1603
+ showAttemptFail: this.logLevel >= 1 /* warn */
1604
+ }) : await getDataFile(prefixes, eKey, "data", this.version.BuildConfig, {
1605
+ showProgress: this.logLevel >= 2 /* info */,
1606
+ showAttemptFail: this.logLevel >= 1 /* warn */
1607
+ });
1578
1608
  const reader = new BLTEReader(blte, eKey, this.keys);
1579
1609
  if (!allowMissingKey) {
1580
1610
  reader.processBytes(allowMissingKey);
@@ -1710,6 +1740,11 @@ class DBDParser {
1710
1740
  }
1711
1741
  });
1712
1742
  }
1743
+ static async parse(wdc) {
1744
+ const parser = new DBDParser(wdc);
1745
+ await parser.init();
1746
+ return parser;
1747
+ }
1713
1748
  getAllIDs() {
1714
1749
  return this.wdc.getAllIDs();
1715
1750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhyster/wow-casc-dbc",
3
- "version": "1.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,10 +37,12 @@
37
37
  "author": "Rhythm",
38
38
  "license": "MIT",
39
39
  "devDependencies": {
40
+ "@types/async": "^3.2.24",
41
+ "@types/cli-progress": "^3.11.5",
40
42
  "@types/jest": "^29.5.12",
41
43
  "@types/node": "^20.11.30",
42
- "@typescript-eslint/eslint-plugin": "^7.3.1",
43
- "@typescript-eslint/parser": "^7.3.1",
44
+ "@typescript-eslint/eslint-plugin": "^7.4.0",
45
+ "@typescript-eslint/parser": "^7.4.0",
44
46
  "eslint": "^8.57.0",
45
47
  "eslint-config-airbnb-base": "^15.0.0",
46
48
  "eslint-config-airbnb-typescript": "^18.0.0",
@@ -49,5 +51,9 @@
49
51
  "ts-node": "^10.9.2",
50
52
  "typescript": "^5.4.3",
51
53
  "unbuild": "^2.0.0"
54
+ },
55
+ "dependencies": {
56
+ "async": "^3.2.5",
57
+ "cli-progress": "^3.12.0"
52
58
  }
53
59
  }