From 1ee166782fce7cbfb82fdac9af6fbd26a870aa0a Mon Sep 17 00:00:00 2001 From: gravel Date: Tue, 26 Dec 2023 14:29:59 +0000 Subject: [PATCH] Improve search & sort performance --- output/js/util.js | 78 +++++++++++++++-------- output/main.js | 92 ++++++++++++++++++--------- sites/+components/tbl-communities.php | 2 +- 3 files changed, 113 insertions(+), 59 deletions(-) diff --git a/output/js/util.js b/output/js/util.js index a076efe..29b8684 100644 --- a/output/js/util.js +++ b/output/js/util.js @@ -77,9 +77,44 @@ export class RoomInfo { return new Date(room.created * 1000); } + static getRoomToken(identifier) { + return identifier.split("+")[0]; + } + static getRoomServerId(identifier) { return identifier.split("+")[1]; } + + static getRoomLanguageFlag(identifier) { + return _RoomInfo.getRoom(identifier).language_flag; + } + + static getRoomName(identifier) { + return _RoomInfo.getRoom(identifier).name; + } + + static getRoomDescription(identifier) { + return _RoomInfo.getRoom(identifier).description; + } + + static getRoomUserCount(identifier) { + return _RoomInfo.getRoom(identifier).active_users; + } + + static getRoomPreviewLink(identifier) { + const server = _RoomInfo.getRoomServer(identifier); + return `${server.base_url}/r/${RoomInfo.getRoomToken(identifier)}`; + } + + static getRoomJoinLink(identifier) { + const server = _RoomInfo.getRoomServer(identifier); + const token = RoomInfo.getRoomToken(identifier); + return `${server.base_url}/${token}?public_key=${server.pubkey}`; + } + + static getRoomHostname(identifier) { + return _RoomInfo.getRoomServer(identifier)?.base_url; + } } export const dom = { @@ -104,20 +139,18 @@ export const dom = { * @param {HTMLTableRowElement} row */ row_info: (row) => { - const joinLink = row.querySelector('.td_join_url a[href]').getAttribute('href'); const identifier = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER); - const joinURL = new URL(joinLink); const dateCreated = RoomInfo.getRoomCreationDate(identifier); /** @type {string[]} */ return { - language_flag: row.querySelector('.td_language').textContent.trim(), - name: row.querySelector('.td_name-inner').textContent.trim(), - description: row.querySelector('.td_description').textContent.trim(), - users: parseFloat(row.querySelector('.td_users').textContent.trim()), - preview_link: row.querySelector('.td_preview a[href]').getAttribute('href'), - join_link: joinLink, + language_flag: RoomInfo.getRoomLanguageFlag(identifier), + name: RoomInfo.getRoomName(identifier), + description: RoomInfo.getRoomDescription(identifier), + users: RoomInfo.getRoomUserCount(identifier), + preview_link: RoomInfo.getRoomPreviewLink(identifier), + join_link: RoomInfo.getRoomJoinLink(identifier), identifier, - hostname: `${joinURL.protocol}//${joinURL.host}`, + hostname: RoomInfo.getRoomHostname(identifier), public_key: RoomInfo.getRoomPublicKey(identifier), staff: RoomInfo.getRoomStaff(identifier), tags: RoomInfo.getRoomTags(identifier), @@ -251,28 +284,15 @@ export function columnIsSortable(column) { ].includes(column); } -/** - * @type {Record any>} - */ -const TRANSFORMATION = { - numeric: (el) => parseInt(el.innerText), - casefold: (el) => el.innerText.toLowerCase().trim(), - getName: (_, row) => dom.row_info(row).name.toLowerCase(), - getServerId: (_, row) => { - const rowInfo = dom.row_info(row); - return RoomInfo.getRoomServerId(rowInfo.identifier); - } -} - /** * @type {Dictionary any>} */ export const COLUMN_TRANSFORMATION = { - [COLUMN.USERS]: TRANSFORMATION.numeric, - [COLUMN.IDENTIFIER]: TRANSFORMATION.casefold, - [COLUMN.NAME]: TRANSFORMATION.getName, - [COLUMN.DESCRIPTION]: TRANSFORMATION.casefold, - [COLUMN.SERVER_ICON]: TRANSFORMATION.getServerId + [COLUMN.USERS]: (identifier) => RoomInfo.getRoomUserCount(identifier), + [COLUMN.IDENTIFIER]: (identifier) => identifier.toLowerCase(), + [COLUMN.NAME]: (identifier) => RoomInfo.getRoomName(identifier).toLowerCase(), + [COLUMN.DESCRIPTION]: (identifier) => RoomInfo.getRoomName(identifier).toLowerCase(), + [COLUMN.SERVER_ICON]: (identifier) => RoomInfo.getRoomServerId(identifier) } /** @@ -300,3 +320,7 @@ export const element = new Proxy({}, { return (...args) => createElement(key, ...args) } }); + +export const unreachable = () => { throw new Error("Unreachable"); }; + +export const workOnMainThread = () => new Promise(resolve => setTimeout(resolve, 0)); diff --git a/output/main.js b/output/main.js index 617a760..775d081 100644 --- a/output/main.js +++ b/output/main.js @@ -17,7 +17,7 @@ import { dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES, columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION, - element, JOIN_URL_PASTE, communityQRCodeURL, STAFF_ID_PASTE, IDENTIFIER_PASTE, DETAILS_LINK_PASTE, CLASSES, flagToLanguageAscii, RoomInfo + element, JOIN_URL_PASTE, communityQRCodeURL, STAFF_ID_PASTE, IDENTIFIER_PASTE, DETAILS_LINK_PASTE, CLASSES, flagToLanguageAscii, RoomInfo, unreachable, workOnMainThread } from './js/util.js'; // Hidden communities for transparency. @@ -539,7 +539,9 @@ function addSearchInteractions() { if (ev.key === "Enter") { this.blur(); } - useSearchTerm(this.value); + if (this.value === "") { + useSearchTerm(""); + } }) dom.btn_clear_search()?.addEventListener('click', function () { @@ -620,12 +622,12 @@ function makeRowComparer(column, ascending) { } // Callback to obtain sortable content from cell text. - const columnToSortable = COLUMN_TRANSFORMATION[column] ?? ((el) => el.innerText.trim()); + const rowToSortable = COLUMN_TRANSFORMATION[column]; // Construct comparer using derived property to determine sort order. const rowComparer = compareProp( ascending ? compareAscending : compareDescending, - row => columnToSortable(row.children[column], row) + ({identifier}) => rowToSortable(identifier) ); return rowComparer; @@ -691,18 +693,25 @@ function markSortableColumns() { } /** - * @type {HTMLTableRowElement[]} + * @type {{row: HTMLTableRowElement, identifier: string}[]} */ const communityFullRowCache = []; +function getAllCachedRows() { + return communityFullRowCache.map(({row}) => row); +} + function initializeSearch() { - communityFullRowCache.push(...dom.tbl_communities_content_rows()); + communityFullRowCache.push(...dom.tbl_communities_content_rows().map(row => ({ + row, + identifier: row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER) ?? unreachable() + }))); } /** * - * @param {string} [rawTerm] + * @param {string} rawTerm */ -function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) { +async function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) { const searchBar = dom.search_bar(); if (searchBar === undefined || !(searchBar instanceof HTMLInputElement)) { @@ -710,47 +719,64 @@ function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) { } if (!rawTerm) { - replaceRowsWith(communityFullRowCache); + replaceRowsWith(getAllCachedRows()); dom.search_bar()?.classList.remove(CLASSES.SEARCH.NO_RESULTS); } else { const term = rawTerm.toLowerCase().replace(/lang:(\S+)/g, "").trim(); const termTags = Array.from(rawTerm.matchAll(/#[^#\s]+/g)).map(match => match[0].slice(1).toLowerCase()); const termLanguage = rawTerm.match(/lang:(\S+)/)?.[1]; - const newRows = communityFullRowCache.filter( - row => { - const rowInfo = dom.row_info(row); - const langAscii = rowInfo.language_flag && flagToLanguageAscii(rowInfo.language_flag).toLowerCase(); - const rowName = rowInfo.name.toLowerCase(); - const rowDesc = rowInfo.description.toLowerCase(); - const rowTags = rowInfo.tags.map(({text}) => text.replace(/\s+/g, "-")); - if (termLanguage && !langAscii.includes(termLanguage.toLowerCase())) { - return false; - } - if (termTags.length >= 1) { - if (termTags.some(tag => rowTags.some(rowTag => rowTag.startsWith(tag)))) { + /** + * @param {{row: HTMLTableRowElement, identifier: string}} rowCache + */ + async function rowMatches(rowCache) { + const {identifier} = rowCache; + const languageFlag = RoomInfo.getRoomLanguageFlag(identifier); + const langAscii = languageFlag && flagToLanguageAscii(languageFlag).toLowerCase(); + if (termLanguage && !langAscii.includes(termLanguage.toLowerCase())) { + return false; + } + const rowName = RoomInfo.getRoomName(identifier).toLowerCase(); + const rowDesc = RoomInfo.getRoomDescription(identifier).toLowerCase(); + if (rowName.includes(term) || rowDesc.includes(term)) { + return true; + } + const rowTags = RoomInfo.getRoomTags(identifier).map(({text}) => text.replace(/\s+/g, "-")); + for (const termTag of termTags) { + for (const rowTag of rowTags) { + if (rowTag.startsWith(termTag)) { return true; } } - return rowName.includes(term) || rowDesc.includes(term); - } - ); + return false; + } + console.time("search"); + const newRowMatches = communityFullRowCache.map(async (rowCache) => ({ rowCache, doesMatch: await rowMatches(rowCache) })); + const newRows = (await Promise.all(newRowMatches)).filter((row) => row.doesMatch).map(({rowCache}) => rowCache.row); + console.timeEnd("search"); if (newRows.length === 0) { searchBar.classList.add(CLASSES.SEARCH.NO_RESULTS); } else { searchBar.classList.remove(CLASSES.SEARCH.NO_RESULTS); } + replaceRowsWith(newRows); } if (fillSearchBarWithTerm) { searchBar.value = rawTerm; } + + sortTable(); } +/** + * @param {HTMLTableRowElement[]} rows + */ function replaceRowsWith(rows) { - dom.tbl_communities_content_rows().forEach(row => row.remove()); - dom.tbl_communities().querySelector("tbody").append(...rows); + const tableBody = dom.tbl_communities()?.querySelector("tbody"); + if (!tableBody) throw new Error("Table body missing") + tableBody.replaceChildren(tableBody.rows[0], ...rows); } /** @@ -761,21 +787,25 @@ function replaceRowsWith(rows) { */ function sortTable(column) { const table = dom.tbl_communities(); + if (!table) throw new Error("Table missing"); const sortState = getSortState(table); const sortingAsBefore = column === undefined; + if (!sortState && !sortingAsBefore) { + throw new Error("Must supply column on first sort"); + } const sortingNewColumn = column !== sortState?.column; - const sortedColumn = column ?? sortState?.column; + const sortedColumn = column ?? sortState?.column ?? unreachable(); const ascending = sortingAsBefore ? - sortState.ascending : ( + sortState?.ascending ?? unreachable() : ( sortingNewColumn ? columnAscendingByDefault(column) - : !sortState.ascending + : sortState?.ascending ?? unreachable() ); const compare = makeRowComparer(sortedColumn, ascending); - const rows = Array.from(table.rows).slice(1); + const rows = dom.tbl_communities_content_rows().map(row => ({row, identifier: row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER)})); rows.sort(compare); - replaceRowsWith(rows); + replaceRowsWith(rows.map(({row}) => row)); setSortState(table, { ascending, column: sortedColumn }); } diff --git a/sites/+components/tbl-communities.php b/sites/+components/tbl-communities.php index 0632d55..7afa176 100644 --- a/sites/+components/tbl-communities.php +++ b/sites/+components/tbl-communities.php @@ -38,7 +38,7 @@ ]; ?> - +
$column): ?> id="th_" class="tbl_communities__th">