From 35a4a7f773acc5259afa22ed7da216c675738f33 Mon Sep 17 00:00:00 2001 From: Jackson Conte Date: Tue, 12 Dec 2023 22:08:31 -0800 Subject: [PATCH] fetch timeout + const linting + various polish Co-authored-by: Varun Singh Co-authored-by: Birdy --- .eslintrc.json | 3 ++ index.html | 2 +- settings.html | 2 +- src/scripts/entity/InstanceEntry.js | 2 +- src/scripts/fetchers/LemmyFetcher.js | 10 +++++-- src/scripts/fetchers/MastodonFetcher.js | 7 ++++- src/scripts/instanceList.js | 10 +++---- src/scripts/pageBuilder.js | 22 ++++++++------ src/scripts/postBuilder/LemmyPostBuilder.js | 2 +- .../{settings-page.js => settingsPage.js} | 30 +++++++++---------- test/paginator.test.js | 8 ++--- test/settings.test.js | 24 +++++++-------- 12 files changed, 70 insertions(+), 52 deletions(-) rename src/scripts/{settings-page.js => settingsPage.js} (60%) diff --git a/.eslintrc.json b/.eslintrc.json index ba25438..1244e85 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,6 +32,9 @@ "no-var": [ "error" ], + "prefer-const": [ + "error" + ], "@stylistic/arrow-spacing": [ "error", { diff --git a/index.html b/index.html index 4224571..88e648f 100644 --- a/index.html +++ b/index.html @@ -38,7 +38,7 @@

Blend

- Page 2 + diff --git a/settings.html b/settings.html index f0cdaf3..af3e9e8 100644 --- a/settings.html +++ b/settings.html @@ -55,7 +55,7 @@

Lemmy

- + diff --git a/src/scripts/entity/InstanceEntry.js b/src/scripts/entity/InstanceEntry.js index cfcea31..e88fe50 100644 --- a/src/scripts/entity/InstanceEntry.js +++ b/src/scripts/entity/InstanceEntry.js @@ -52,7 +52,7 @@ export class InstanceEntry extends HTMLElement { `; - let script = document.createElement("script"); + const script = document.createElement("script"); script.type = "module"; script.textContent = ` import { handleRemoveInstance } from './src/scripts/instanceList.js'; diff --git a/src/scripts/fetchers/LemmyFetcher.js b/src/scripts/fetchers/LemmyFetcher.js index 29e18d2..7ec495b 100644 --- a/src/scripts/fetchers/LemmyFetcher.js +++ b/src/scripts/fetchers/LemmyFetcher.js @@ -12,11 +12,17 @@ export class LemmyFetcher extends Fetcher { const posts = []; try { const url = instanceUrl + API_V3 + "&limit=" + NUM_POSTS; - const response = await fetch(url); + + const timeoutDelay = 10000; + const response = await Promise.race([ + fetch(url), + new Promise((_resolve, reject) => setTimeout(() => reject("Fetch calls timed out."), timeoutDelay)), + ]); + if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } - let response_data = await response.json(); + const response_data = await response.json(); const posts_json = response_data.posts; posts_json.forEach((post) => { posts.push(post); diff --git a/src/scripts/fetchers/MastodonFetcher.js b/src/scripts/fetchers/MastodonFetcher.js index aad3af4..c004b5f 100644 --- a/src/scripts/fetchers/MastodonFetcher.js +++ b/src/scripts/fetchers/MastodonFetcher.js @@ -13,7 +13,12 @@ export class MastodonFetcher extends Fetcher { try { const hashtags = await this.#fetchTrendingTags(instURL + TAGS_SUFFIX); const fetchPromises = hashtags.map(tag => this.#fetchPostsByHashtag(instURL, tag.name)); - const responses = await Promise.all(fetchPromises); + const timeoutDelay = 10000; + const responses = await Promise.race( + [ + Promise.all(fetchPromises), + new Promise((_resolve, reject) => setTimeout(() => reject("Fetch calls timed out."), timeoutDelay)), + ]); const posts = responses.flat(); posts.push(...posts); return posts; diff --git a/src/scripts/instanceList.js b/src/scripts/instanceList.js index b38b9e4..eef9d2b 100644 --- a/src/scripts/instanceList.js +++ b/src/scripts/instanceList.js @@ -45,7 +45,7 @@ export function saveLists(instanceLists, storage = localStorage) { export async function addInstance(network, url, storage = localStorage) { if (!(ALLOWED_NETWORKS.has(network)) || !validUrl(url)) return false; - let instanceList = fetchInstanceLists(storage); + const instanceList = fetchInstanceLists(storage); // network is already guaranteed to be in ALLOWED_NETWORKS and thus on the default list instanceList[network].unshift(url); storage.setItem(INST_LISTS, JSON.stringify(instanceList)); @@ -59,9 +59,9 @@ export async function addInstance(network, url, storage = localStorage) { * @param {Storage} storage */ export function handleRemoveInstance(network, url, storage = localStorage) { - let i = removeInstance(network, url, storage); + const i = removeInstance(network, url, storage); if (i !== -1) { - let list = document.getElementById(`${network}-instance-list`); + const list = document.getElementById(`${network}-instance-list`); // first child will always be add instance input box // index 0 of instance list will be child 1 list.removeChild(list.children[i + 1]); @@ -75,9 +75,9 @@ export function handleRemoveInstance(network, url, storage = localStorage) { * @returns {number} Index removed if successful, -1 otherwise */ export function removeInstance(network, url, storage = localStorage) { - let instanceLists = fetchInstanceLists(storage); + const instanceLists = fetchInstanceLists(storage); if (network in instanceLists && instanceLists[network].includes(url)) { - let ind = instanceLists[network].indexOf(url); + const ind = instanceLists[network].indexOf(url); instanceLists[network].splice(ind, 1); saveLists(instanceLists, storage); return ind; diff --git a/src/scripts/pageBuilder.js b/src/scripts/pageBuilder.js index debd355..ab876d3 100644 --- a/src/scripts/pageBuilder.js +++ b/src/scripts/pageBuilder.js @@ -19,25 +19,29 @@ export async function buildPage(HANDLERS) { nextPage.disabled = true; prevPage.disabled = true; - let postsByNetwork = {}; + const postsByNetwork = {}; let maxLengthArray = 0; const instLists = fetchInstanceLists(); - for (let [network, instanceList] of Object.entries(instLists)) { - let fetcher = new HANDLERS[network]["fetcher"](); - let postBuilder = new HANDLERS[network]["postBuilder"](); + // Iterate over networks (currently just Mastodon and Lemmy) + for (const [network, instanceList] of Object.entries(instLists)) { + // Get object handlers for API and post formatting + const fetcher = new HANDLERS[network]["fetcher"](); + const postBuilder = new HANDLERS[network]["postBuilder"](); postsByNetwork[network] = []; - for (let url of instanceList) { - let res = await fetcher.fetchPosts(url); - for (let post of res) { + // Iterate over each network's instance list + for (const url of instanceList) { + const res = await fetcher.fetchPosts(url); + if (res === null) continue; + for (const post of res) { postsByNetwork[network].push(postBuilder.buildPost(post)); } } maxLengthArray = Math.max(maxLengthArray, postsByNetwork[network].length); } - let posts = []; + const posts = []; for (let i = 0; i < maxLengthArray; i++) { - for (let [network] of Object.entries(instLists)) { + for (const [network] of Object.entries(instLists)) { if (i < postsByNetwork[network].length) { posts.push(postsByNetwork[network][i]); } diff --git a/src/scripts/postBuilder/LemmyPostBuilder.js b/src/scripts/postBuilder/LemmyPostBuilder.js index 088ca21..6bd4635 100644 --- a/src/scripts/postBuilder/LemmyPostBuilder.js +++ b/src/scripts/postBuilder/LemmyPostBuilder.js @@ -18,7 +18,7 @@ export class LemmyPostBuilder extends PostBuilder { * @returns {Object}- A Post element created from raw json data from the API */ buildPost(post) { - let newPost = document.createElement("fedi-post"); + const newPost = document.createElement("fedi-post"); newPost.setAttribute("id", post.post.id); newPost.setAttribute("content", this.#extractPostContent(post.post)); // Either a url or body or both. newPost.setAttribute("author-name", post.creator.name); diff --git a/src/scripts/settings-page.js b/src/scripts/settingsPage.js similarity index 60% rename from src/scripts/settings-page.js rename to src/scripts/settingsPage.js index 6a319ea..ed9941d 100644 --- a/src/scripts/settings-page.js +++ b/src/scripts/settingsPage.js @@ -6,12 +6,12 @@ import { InstanceEntry } from "./entity/InstanceEntry.js"; import { fetchInstanceLists, saveLists, addInstance, DEFAULT_LISTS } from "./instanceList.js"; // Populate instance lists onto UI -let instLists = fetchInstanceLists(); -for (let [network, list] of Object.entries(instLists)) { - let parent = document.getElementById(`${network}-instance-list`); - let listBottom = parent.querySelector(".instances-reset-button"); +const instLists = fetchInstanceLists(); +for (const [network, list] of Object.entries(instLists)) { + const parent = document.getElementById(`${network}-instance-list`); + const listBottom = parent.querySelector(".instances-reset-button"); list.forEach((url) => { - let newEntry = new InstanceEntry(); + const newEntry = new InstanceEntry(); newEntry.network = network; newEntry.url = url; parent.insertBefore(newEntry, listBottom); @@ -20,24 +20,24 @@ for (let [network, list] of Object.entries(instLists)) { // Add event listners for add instance buttons. // Remove instance buttons handled by InstanceEntry.js -let addInstBtns = Array.from(document.getElementsByClassName("add-instance-btn")); +const addInstBtns = Array.from(document.getElementsByClassName("add-instance-btn")); addInstBtns.forEach((element) => { // event handler is async depending on whether or not we re-add instance validation via fetch element.addEventListener("click", async (event) => { - let btn = event.currentTarget; - let network = btn.getAttribute("data-network"); - let input = document.getElementById(`${network}-add-instance`); + const btn = event.currentTarget; + const network = btn.getAttribute("data-network"); + const input = document.getElementById(`${network}-add-instance`); let url = input.value; // add https:// if the user forgot if (!(url.includes("//"))) { url = "https://".concat(url); } - let success = await addInstance(network, url); + const success = await addInstance(network, url); if (success) { - let newEntry = new InstanceEntry(); + const newEntry = new InstanceEntry(); newEntry.network = network; newEntry.url = url; - let list = document.getElementById(`${network}-instance-list`); + const list = document.getElementById(`${network}-instance-list`); list.insertBefore(newEntry, list.children[1]); input.value = ""; // clear input box after adding to UI } @@ -48,11 +48,11 @@ addInstBtns.forEach((element) => { }); // Add event listeners for "reset to default" -let resetDefaultBtns = Array.from(document.getElementsByClassName("instances-reset-button")); +const resetDefaultBtns = Array.from(document.getElementsByClassName("instances-reset-button")); resetDefaultBtns.forEach((element) => { element.addEventListener("click", (event) => { - let network = event.currentTarget.getAttribute("data-network"); - let instanceLists = fetchInstanceLists(); + const network = event.currentTarget.getAttribute("data-network"); + const instanceLists = fetchInstanceLists(); instanceLists[network] = DEFAULT_LISTS[network]; saveLists(instanceLists); location.reload(); diff --git a/test/paginator.test.js b/test/paginator.test.js index fbe6b1a..24815bd 100644 --- a/test/paginator.test.js +++ b/test/paginator.test.js @@ -69,7 +69,7 @@ describe("Paginator", () => { paginator = new paginatorImport.Paginator(posts, 2); // Should display the first two posts - let featuredTagsPosts = document.getElementById("featuredTagsPosts"); + const featuredTagsPosts = document.getElementById("featuredTagsPosts"); assert.strictEqual(featuredTagsPosts.children.length, 2); assert.strictEqual(pageLabel.innerHTML, "Page 1 of 2"); }); @@ -92,7 +92,7 @@ describe("Paginator", () => { paginator = new paginatorImport.Paginator(posts, 2); // Should display the first two posts - before clicking next - let featuredTagsPosts = document.getElementById("featuredTagsPosts"); + const featuredTagsPosts = document.getElementById("featuredTagsPosts"); assert.strictEqual(featuredTagsPosts.children.length, 2); // Should display the next two posts (well actually one since length is 3) @@ -118,7 +118,7 @@ describe("Paginator", () => { paginator = new paginatorImport.Paginator(posts, 2); // Should display the first two posts - before clicking next - let featuredTagsPosts = document.getElementById("featuredTagsPosts"); + const featuredTagsPosts = document.getElementById("featuredTagsPosts"); assert.strictEqual(featuredTagsPosts.children.length, 2); // Should stay at page 1 @@ -144,7 +144,7 @@ describe("Paginator", () => { paginator = new paginatorImport.Paginator(posts, 2); // Should display the first two posts - before clicking next - let featuredTagsPosts = document.getElementById("featuredTagsPosts"); + const featuredTagsPosts = document.getElementById("featuredTagsPosts"); assert.strictEqual(featuredTagsPosts.children.length, 2); // Should display the next two posts (well actually one since length is 3) diff --git a/test/settings.test.js b/test/settings.test.js index 42eb806..7a979e9 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -3,7 +3,7 @@ import * as assert from "assert"; import * as instanceList from "../src/scripts/instanceList.js"; // Mock localStorage -let mockStorage = { +const mockStorage = { store: {}, getItem(key) { return (key in this.store) ? this.store[key] : null; @@ -18,21 +18,21 @@ let mockStorage = { const lists = instanceList.fetchInstanceLists(mockStorage); describe("Fetching instance lists", () => { it("Should return default lists", () => { - for (let [network, list] of Object.entries(lists)) { + for (const [network, list] of Object.entries(lists)) { list.forEach(url => assert.ok(instanceList.DEFAULT_LISTS[network].includes(url))); } }); }); describe("Removing instances", () => { it("Don't remove an instance that isn't there", () => { - let ind = instanceList.removeInstance("mastodon", "ligma", mockStorage); - let l2 = instanceList.fetchInstanceLists(mockStorage); + const ind = instanceList.removeInstance("mastodon", "ligma", mockStorage); + const l2 = instanceList.fetchInstanceLists(mockStorage); assert.strictEqual(ind, -1); assert.strictEqual(l2["mastodon"].length, lists["mastodon"].length); }); it("Allows removal of a default instance", () => { - let ind = instanceList.removeInstance("mastodon", instanceList.DEFAULT_LISTS["mastodon"][0], mockStorage); - let l2 = instanceList.fetchInstanceLists(mockStorage); + const ind = instanceList.removeInstance("mastodon", instanceList.DEFAULT_LISTS["mastodon"][0], mockStorage); + const l2 = instanceList.fetchInstanceLists(mockStorage); assert.strictEqual(ind, 0); assert.strictEqual(l2["mastodon"].length, lists["mastodon"].length - 1); }); @@ -48,16 +48,16 @@ describe("Adding instances", () => { // assert.strictEqual(before['lemmy'].length, after['lemmy'].length); // }); it("Reject a malformed URL", async () => { - let before = instanceList.fetchInstanceLists(mockStorage); - let success = await instanceList.addInstance("lemmy", "skidibibopmdada", mockStorage); - let after = instanceList.fetchInstanceLists(mockStorage); + const before = instanceList.fetchInstanceLists(mockStorage); + const success = await instanceList.addInstance("lemmy", "skidibibopmdada", mockStorage); + const after = instanceList.fetchInstanceLists(mockStorage); assert.strictEqual(success, false); assert.strictEqual(before["lemmy"].length, after["lemmy"].length); }); it("Accept a good URL", async () => { - let before = instanceList.fetchInstanceLists(mockStorage); - let success = await instanceList.addInstance("lemmy", "https://mtgzone.com", mockStorage); - let after = instanceList.fetchInstanceLists(mockStorage); + const before = instanceList.fetchInstanceLists(mockStorage); + const success = await instanceList.addInstance("lemmy", "https://mtgzone.com", mockStorage); + const after = instanceList.fetchInstanceLists(mockStorage); assert.strictEqual(success, true); assert.strictEqual(before["lemmy"].length + 1, after["lemmy"].length); });