Skip to content

Commit ae02245

Browse files
committed
feat: reworked db's fetching method
1 parent 76a10c8 commit ae02245

File tree

2 files changed

+311
-14
lines changed

2 files changed

+311
-14
lines changed

db/TDSWikiFetcher.js

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,37 @@ class TDSWikiFetcher {
4141
}
4242
}
4343

44-
async fetchTowers() {
44+
/**
45+
* Fetch tower list + enrich each tower.
46+
*
47+
* @param {object} [options]
48+
* @param {(payload: {
49+
* phase: "list" | "enrich",
50+
* tower?: any,
51+
* index?: number,
52+
* total?: number,
53+
* completed?: number,
54+
* pending?: number,
55+
* percent?: number,
56+
* isDone?: boolean,
57+
* name?: string,
58+
* pageTitle?: string,
59+
* error?: any
60+
* }) => void} [options.onProgress]
61+
* @param {number} [options.concurrency=5]
62+
* @returns {Promise<any[]>}
63+
*/
64+
async fetchTowers(options = {}) {
65+
const { onProgress, concurrency = 5 } = options;
66+
4567
try {
4668
console.log("fetching towers from wiki API...");
4769

70+
const safeConcurrency =
71+
Number.isFinite(concurrency) && concurrency > 0
72+
? Math.floor(concurrency)
73+
: 5;
74+
4875
const data = await this.fetchFromApi({
4976
page: this.sourcePage,
5077
prop: "text",
@@ -106,9 +133,82 @@ class TDSWikiFetcher {
106133
};
107134
});
108135

109-
await Promise.allSettled(
110-
towers.map((tower) => this.enrichTowerData(tower)),
111-
);
136+
if (typeof onProgress === "function") {
137+
try {
138+
const total = towers.length;
139+
onProgress({
140+
phase: "list",
141+
total,
142+
completed: 0,
143+
pending: total,
144+
percent: 0,
145+
isDone: total === 0,
146+
});
147+
} catch (e) {}
148+
}
149+
150+
const total = towers.length;
151+
let completed = 0;
152+
153+
const makeSummary = () => {
154+
const pending = Math.max(0, total - completed);
155+
const percent =
156+
total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0;
157+
158+
return {
159+
total,
160+
completed,
161+
pending,
162+
percent,
163+
isDone: completed >= total,
164+
};
165+
};
166+
167+
const worker = async (startIndex) => {
168+
for (let i = startIndex; i < towers.length; i += safeConcurrency) {
169+
const tower = towers[i];
170+
try {
171+
await this.enrichTowerData(tower);
172+
completed++;
173+
174+
if (typeof onProgress === "function") {
175+
try {
176+
onProgress({
177+
phase: "enrich",
178+
tower,
179+
index: i,
180+
name: tower?.name,
181+
pageTitle: tower?.pageTitle,
182+
...makeSummary(),
183+
});
184+
} catch (e) {}
185+
}
186+
} catch (error) {
187+
completed++;
188+
189+
if (typeof onProgress === "function") {
190+
try {
191+
onProgress({
192+
phase: "enrich",
193+
tower,
194+
index: i,
195+
name: tower?.name,
196+
pageTitle: tower?.pageTitle,
197+
error,
198+
...makeSummary(),
199+
});
200+
} catch (e) {}
201+
}
202+
}
203+
}
204+
};
205+
206+
const workers = [];
207+
for (let w = 0; w < Math.min(safeConcurrency, towers.length); w++) {
208+
workers.push(worker(w));
209+
}
210+
211+
await Promise.allSettled(workers);
112212

113213
return towers;
114214
} catch (error) {

db/TDSWikiLoader.js

Lines changed: 207 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,42 @@ document.addEventListener("DOMContentLoaded", function () {
234234
refreshButton.innerHTML =
235235
'<i class="spinner-border spinner-border-sm me-2"></i>Refreshing...';
236236

237+
const featuredContainer = document.querySelector(".featured-towers");
238+
const allTowersContainer = document.getElementById("all-towers");
239+
240+
const loadingBanner = document.createElement("div");
241+
loadingBanner.className = "col-12 mb-3";
242+
const loadingId = `loading-${Date.now()}`;
243+
loadingBanner.innerHTML = `
244+
<div id="${loadingId}" class="card h-100 bg-dark bg-gradient text-white">
245+
<div class="card-body d-flex align-items-center justify-content-between gap-3">
246+
<div class="d-flex align-items-center gap-2">
247+
<div class="spinner-border spinner-border-sm text-info" role="status" aria-hidden="true"></div>
248+
<div>
249+
<div class="fw-bold">Commander, get me the towers before I (the) maul you...</div>
250+
<div class="small text-muted">
251+
<span data-loading-status>Starting…</span>
252+
</div>
253+
</div>
254+
</div>
255+
256+
<div class="text-end">
257+
<div class="fw-bold"><span data-loading-count>0</span>/<span data-loading-total>?</span></div>
258+
<div class="progress mt-1" style="width: 180px; height: 8px;">
259+
<div class="progress-bar bg-info" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
260+
</div>
261+
</div>
262+
</div>
263+
</div>
264+
`;
265+
237266
try {
238267
window.originalCardsOrder = [];
239268

240269
if (!forceRefresh) {
241270
const cachedData = localStorage.getItem("towerDataCache");
242271
const cacheTimestamp = localStorage.getItem("towerDataTimestamp");
243272

244-
// use cache if it exists
245273
if (cachedData && cacheTimestamp) {
246274
const cacheAge = Date.now() - parseInt(cacheTimestamp);
247275
const cacheMaxAge = 12 * 60 * 60 * 1000; // 12 hours (modify the first number only)
@@ -261,22 +289,191 @@ document.addEventListener("DOMContentLoaded", function () {
261289
}
262290
}
263291

264-
document.getElementById("featured-towers").innerHTML =
265-
'<div class="col-12 text-center text-light p-5"><div class="spinner-border" role="status"></div><p class="mt-2">Commander get the highlights before I sell you to Lord Exo.</p></div>';
292+
featuredContainer.innerHTML = "";
293+
allTowersContainer.innerHTML = "";
294+
295+
featuredContainer.innerHTML =
296+
'<div class="col-12 text-center text-light p-3 small text-muted">Loading highlights…</div>';
297+
298+
allTowersContainer.appendChild(loadingBanner);
299+
300+
const bannerEl = document.getElementById(loadingId);
301+
const statusEl = bannerEl?.querySelector("[data-loading-status]");
302+
const countEl = bannerEl?.querySelector("[data-loading-count]");
303+
const totalEl = bannerEl?.querySelector("[data-loading-total]");
304+
const barEl = bannerEl?.querySelector(".progress-bar");
305+
306+
const setProgress = (loaded, total, statusText) => {
307+
if (typeof statusText === "string" && statusEl)
308+
statusEl.textContent = statusText;
309+
if (countEl) countEl.textContent = String(loaded);
310+
if (totalEl) totalEl.textContent = total > 0 ? String(total) : "?";
311+
if (barEl) {
312+
const pct =
313+
total > 0 ? Math.min(100, Math.round((loaded / total) * 100)) : 0;
314+
barEl.style.width = `${pct}%`;
315+
barEl.setAttribute("aria-valuenow", String(pct));
316+
}
317+
};
318+
319+
const data = await wikiFetcher.fetchFromApi({
320+
page: wikiFetcher.sourcePage,
321+
prop: "text",
322+
});
323+
if (data.error) throw new Error(data.error.info);
324+
325+
const htmlContent = data.parse?.text?.["*"];
326+
if (!htmlContent) throw new Error("No source page content");
327+
328+
const parser = new DOMParser();
329+
const doc = parser.parseFromString(
330+
`<body>${htmlContent}</body>`,
331+
"text/html",
332+
);
333+
const towerElements = doc.querySelectorAll(".CategoryTreeItem");
334+
335+
const baseTowers = Array.from(towerElements)
336+
.filter((element) =>
337+
element.querySelector("a")?.href.includes("User_blog:"),
338+
)
339+
.map((element) => {
340+
const link = element.querySelector("a");
341+
let fullText = link?.textContent?.trim() || "Unknown Tower";
342+
343+
if (fullText.startsWith("User blog:"))
344+
fullText = fullText.replace("User blog:", "");
345+
346+
const towerName = fullText.includes("/")
347+
? fullText.split("/").pop()
348+
: fullText;
349+
const href = link?.getAttribute("href") || "";
350+
const pageTitle = href.replace(/^\/wiki\//, "") || fullText;
351+
352+
return {
353+
name: towerName,
354+
pageTitle: decodeURIComponent(pageTitle),
355+
url: href,
356+
image:
357+
"https://static.wikia.nocookie.net/tower-defense-sim/images/4/4a/Site-favicon.ico",
358+
author: fullText.includes("/")
359+
? fullText.split("/")[0]
360+
: "Wiki Contributor",
361+
featured: wikiFetcher.featuredTowers.includes(fullText),
362+
highlighted: window.highlights
363+
? window.highlights.includes(fullText)
364+
: false,
365+
verified: window.approvedTowers
366+
? window.approvedTowers.includes(fullText)
367+
: false,
368+
unverified: window.approvedTowers
369+
? !window.approvedTowers.includes(fullText)
370+
: true,
371+
grandfathered: window.grandfatheredTowers
372+
? window.grandfatheredTowers.includes(fullText)
373+
: false,
374+
uploadDate: "Recently",
375+
};
376+
});
377+
378+
const total = baseTowers.length;
379+
setProgress(0, total, "Fetching tower pages…");
380+
381+
featuredContainer.innerHTML = "";
382+
const anyHighlightsExpected = baseTowers.some((t) => t.highlighted);
383+
if (!anyHighlightsExpected) {
384+
featuredContainer.innerHTML =
385+
'<div class="col-12 text-center text-light p-3">No highlights at this time.</div>';
386+
}
387+
388+
const concurrency = 5;
389+
const streamedTowers = [];
390+
const renderedFeaturedNames = new Set();
391+
const renderedMainNames = new Set();
392+
let completed = 0;
266393

267-
document.getElementById("all-towers").innerHTML =
268-
'<div class="col-12 text-center text-light p-5"><div class="spinner-border" role="status"></div><p class="mt-2">Loading towers from the TDS Wiki...</p></div>';
394+
const onTowerReady = (tower) => {
395+
streamedTowers.push(tower);
269396

270-
const towers = await wikiFetcher.fetchTowers(forceRefresh);
397+
// featured/highlighted render
398+
if (
399+
tower.highlighted &&
400+
!renderedFeaturedNames.has(tower.pageTitle || tower.name)
401+
) {
402+
renderTowerCard(tower, featuredContainer);
403+
renderedFeaturedNames.add(tower.pageTitle || tower.name);
404+
}
405+
406+
// main list render
407+
const mainKey = tower.pageTitle || tower.name;
408+
if (!renderedMainNames.has(mainKey)) {
409+
renderTowerCard(tower, allTowersContainer);
410+
renderedMainNames.add(mainKey);
411+
}
412+
};
413+
414+
let nextIndex = 0;
415+
416+
const worker = async () => {
417+
while (true) {
418+
const i = nextIndex++;
419+
if (i >= baseTowers.length) return;
420+
421+
const tower = baseTowers[i];
422+
setProgress(completed, total, `Fetching… ${tower.name}`);
423+
424+
try {
425+
await wikiFetcher.enrichTowerData(tower);
426+
} catch (e) {
427+
console.log(`Failed to fetch tower ${tower.name}:`, e);
428+
}
429+
430+
completed++;
431+
onTowerReady(tower);
432+
setProgress(completed, total, `Loaded ${completed}/${total}`);
271433

272-
localStorage.setItem("towerDataCache", JSON.stringify(towers));
434+
// Yield so UI repaints promptly (especially on slower machines)
435+
await new Promise((r) => setTimeout(r, 0));
436+
}
437+
};
438+
439+
const workers = [];
440+
for (let w = 0; w < Math.min(concurrency, baseTowers.length); w++) {
441+
workers.push(worker());
442+
}
443+
444+
await Promise.allSettled(workers);
445+
446+
localStorage.setItem("towerDataCache", JSON.stringify(streamedTowers));
273447
localStorage.setItem("towerDataTimestamp", Date.now().toString());
274448

275-
renderAllTowers(towers);
449+
if (window.setupSearch) window.setupSearch();
450+
if (window.applyFilters) window.applyFilters();
451+
if (window.setupSorting) window.setupSorting();
452+
453+
setProgress(total, total, "Done");
454+
if (bannerEl) {
455+
bannerEl.classList.add("border", "border-success");
456+
const spinner = bannerEl.querySelector(".spinner-border");
457+
if (spinner) spinner.remove();
458+
}
459+
460+
setTimeout(() => {
461+
const el = document.getElementById(loadingId);
462+
if (el && el.parentNode) {
463+
el.parentNode.removeChild(el);
464+
}
465+
}, 1200);
276466
} catch (error) {
277467
console.error("Failed to load towers:", error);
278-
document.getElementById("all-towers").innerHTML =
279-
'<div class="col-12 text-center text-danger p-5"><i class="bi bi-exclamation-triangle-fill fs-1"></i><p class="mt-2">Failed to load towers from the wiki. Please try again later.</p></div>';
468+
469+
allTowersContainer.innerHTML = `
470+
<div class="col-12">
471+
<div class="alert alert-danger">
472+
<div class="fw-bold"><i class="bi bi-exclamation-triangle-fill me-2"></i>Failed to load towers from the wiki.</div>
473+
<div class="small">Please try again later.</div>
474+
</div>
475+
</div>
476+
`;
280477
} finally {
281478
refreshButton.disabled = false;
282479
refreshButton.innerHTML =

0 commit comments

Comments
 (0)