From 6e3900cb79be066f5c6140af041aa8eeeed5cf8d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:11:58 +0000 Subject: [PATCH] perf(render): pre-calculate index and format fields on load Co-authored-by: MrAlokTech <107493955+MrAlokTech@users.noreply.github.com> --- .jules/bolt.md | 3 ++ script.js | 65 +++++++++++++---------- tests/test_prepareSearchIndex.js | 80 ++++++++++++++++++++++++++++ tests/test_render_pdfs.js | 91 ++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 27 deletions(-) create mode 100644 .jules/bolt.md create mode 100644 tests/test_prepareSearchIndex.js create mode 100644 tests/test_render_pdfs.js diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..693d89c --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-05-18 - [Pre-calculating derived properties] +**Learning:** Moving date parsing, string concatenation, and lowercasing out of the render loop (which runs on every keystroke) into a one-time data load step significantly reduces CPU overhead and avoids unnecessary garbage collection during search filtering. +**Action:** Use a `prepareSearchIndex` function to map over incoming data to add runtime properties like `_searchStr`, `_formattedDate`, and `_isNew`. diff --git a/script.js b/script.js index bdc06f3..596e5d9 100644 --- a/script.js +++ b/script.js @@ -335,6 +335,27 @@ function getAdData(slotName) { /* ========================================= 5. DATA LOADING WITH CACHING ========================================= */ +function prepareSearchIndex(pdf) { + // Check for Firestore Timestamps (via .toDate()) before fallback to standard Date parsing + const uploadDateObj = pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function' + ? pdf.uploadDate.toDate() + : new Date(pdf.uploadDate); + + // _searchStr is a lowercased concatenation of searchable fields + pdf._searchStr = `${pdf.title} ${pdf.description} ${pdf.category} ${pdf.author}`.toLowerCase(); + + // Formatting Date once + pdf._formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + + // Calculate if it's new once + const timeDiff = new Date() - uploadDateObj; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + + return pdf; +} + function renderSemesterTabs() { const container = document.getElementById('semesterTabsContainer'); if (!container) return; @@ -450,7 +471,7 @@ async function loadPDFDatabase() { } if (shouldUseCache) { - pdfDatabase = cachedData; + pdfDatabase = cachedData.map(prepareSearchIndex); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -469,6 +490,9 @@ async function loadPDFDatabase() { pdfDatabase.push({ id: doc.id, ...doc.data() }); }); + // Mapping to calculate _searchStr before saving to local storage so its already computed + pdfDatabase = pdfDatabase.map(prepareSearchIndex); + localStorage.setItem(CACHE_KEY, JSON.stringify({ timestamp: new Date().getTime(), data: pdfDatabase @@ -902,26 +926,22 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + // EARLY RETURNS: Check cheapest equality matches first + if (pdf.class !== currentClass) return false; + if (pdf.semester !== currentSemester) return false; - // NEW: Check if the PDF class matches the UI's current class selection - // Note: If old documents don't have this field, they will be hidden. - const matchesClass = pdf.class === currentClass; - - let matchesCategory = false; if (currentCategory === 'favorites') { - matchesCategory = favorites.includes(pdf.id); - } else { - matchesCategory = currentCategory === 'all' || pdf.category === currentCategory; + if (!favorites.includes(pdf.id)) return false; + } else if (currentCategory !== 'all') { + if (pdf.category !== currentCategory) return false; } - const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) || - pdf.description.toLowerCase().includes(searchTerm) || - pdf.category.toLowerCase().includes(searchTerm) || - pdf.author.toLowerCase().includes(searchTerm); + // Fast search using pre-calculated _searchStr + if (searchTerm && !pdf._searchStr.includes(searchTerm)) { + return false; + } - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -991,11 +1011,7 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { const heartIconClass = isFav ? 'fas' : 'far'; const btnActiveClass = isFav ? 'active' : ''; - const uploadDateObj = new Date(pdf.uploadDate); - const timeDiff = new Date() - uploadDateObj; - const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days - - const newBadgeHTML = isNew + const newBadgeHTML = pdf._isNew ? `NEW` : ''; @@ -1007,11 +1023,6 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { }; const categoryIcon = categoryIcons[pdf.category] || 'fa-file-pdf'; - // Formatting Date - const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', { - year: 'numeric', month: 'short', day: 'numeric' - }); - // Uses global escapeHtml() now const highlightText = (text) => { @@ -1033,7 +1044,7 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) {
${escapeHtml(pdf.category)}
-
${formattedDate}
+
${pdf._formattedDate}

${highlightText(pdf.description)}

diff --git a/tests/test_prepareSearchIndex.js b/tests/test_prepareSearchIndex.js new file mode 100644 index 0000000..0258c35 --- /dev/null +++ b/tests/test_prepareSearchIndex.js @@ -0,0 +1,80 @@ +const assert = require('assert'); +const fs = require('fs'); +const vm = require('vm'); + +// Extract the prepareSearchIndex function from script.js +const scriptContent = fs.readFileSync('script.js', 'utf8'); + +// A simple regex to grab the function body +const functionMatch = scriptContent.match(/function prepareSearchIndex\([\s\S]*?\n}/); + +if (!functionMatch) { + console.error("Could not find prepareSearchIndex in script.js"); + process.exit(1); +} + +const context = { console }; +vm.createContext(context); +vm.runInContext(functionMatch[0], context); + +// Define test cases +const tests = [ + { + name: "Basic formatting", + input: { + title: "Test PDF", + description: "A description", + category: "Physics", + author: "John Doe", + uploadDate: "2023-01-01T12:00:00Z" + }, + verify: (result) => { + assert.strictEqual(result._searchStr, "test pdf a description physics john doe"); + assert.strictEqual(result._formattedDate, "Jan 1, 2023"); // Format depends on local timezone but generally this + assert.strictEqual(result._isNew, false); + } + }, + { + name: "New document", + input: { + title: "New PDF", + description: "New", + category: "Math", + author: "Jane Doe", + uploadDate: new Date().toISOString() + }, + verify: (result) => { + assert.strictEqual(result._isNew, true); + } + }, + { + name: "Firestore timestamp format", + input: { + title: "Firestore PDF", + description: "Desc", + category: "Chemistry", + author: "Bob", + uploadDate: { + toDate: () => new Date("2023-05-15T12:00:00Z") + } + }, + verify: (result) => { + assert.strictEqual(result._formattedDate, "May 15, 2023"); + } + } +]; + +let failed = false; +for (const test of tests) { + try { + const result = context.prepareSearchIndex({...test.input}); + test.verify(result); + console.log(`✅ ${test.name}`); + } catch (e) { + console.error(`❌ ${test.name} failed:`, e.message); + failed = true; + } +} + +if (failed) process.exit(1); +console.log("All prepareSearchIndex tests passed!"); diff --git a/tests/test_render_pdfs.js b/tests/test_render_pdfs.js new file mode 100644 index 0000000..6d91abb --- /dev/null +++ b/tests/test_render_pdfs.js @@ -0,0 +1,91 @@ +const assert = require('assert'); +const fs = require('fs'); +const vm = require('vm'); + +const scriptContent = fs.readFileSync('script.js', 'utf8'); + +const context = { + document: { + getElementById: () => ({ + value: 'quantum', + textContent: '', + style: { display: '' }, + innerHTML: '', + classList: { add: () => {}, remove: () => {} } + }), + querySelectorAll: () => [] + }, + setTimeout: (fn) => fn(), + clearTimeout: () => {}, + localStorage: { + getItem: () => null, + setItem: () => {} + }, + console: console, + fetch: () => {}, + window: {}, + navigator: { userAgent: '' } +}; + +vm.createContext(context); +try { + vm.runInContext(` + // Stub missing functions from script.js that renderPDFs might call + function logInteraction() {} + function createPDFCard() { return "cardHTML"; } + function createAdHTML() { return ""; } + function createFallbackHTML() { return ""; } + function getAdData() { return null; } + function getFavorites() { return []; } + function updatePDFCount(c) { filteredCount = c; } + + var currentClass = 'MSc Chemistry'; + var currentSemester = 1; + var currentCategory = 'all'; + var searchTimeout; + var pdfDatabase = []; + var emptyState = document.getElementById('emptyState'); + var pdfGrid = document.getElementById('pdfGrid'); + var pdfCount = document.getElementById('pdfCount'); + var searchInput = document.getElementById('searchInput'); + var GAS_URL = ''; + var filteredCount = 0; + + // Extract the renderPDFs function body + ${scriptContent.match(/function renderPDFs\([\s\S]*?^}/m)[0]} + + `, context); +} catch (e) { + console.error("Setup error:", e); +} + +// Test with mock data +context.pdfDatabase = [ + { + id: "1", + title: "Quantum Mechanics", + category: "Physics", + class: "MSc Chemistry", + semester: 1, + _searchStr: "quantum mechanics physics dr. smith" + }, + { + id: "2", + title: "Organic Synthesis", + category: "Organic", + class: "MSc Chemistry", + semester: 1, + _searchStr: "organic synthesis organic dr. jones" + } +]; + +context.renderPDFs(); +console.log("Filtered count after search for 'quantum':", context.filteredCount); + +context.searchInput.value = 'invalidsearch'; +context.renderPDFs(); +console.log("Filtered count after invalid search:", context.filteredCount); + +context.searchInput.value = ''; +context.renderPDFs(); +console.log("Filtered count after empty search:", context.filteredCount);