From 484e75c0c8ee0710739d7d45833253a4fda9e04d Mon Sep 17 00:00:00 2001 From: kubrickcode Date: Wed, 8 Oct 2025 09:04:35 +0000 Subject: [PATCH] Fix badge display on initial page load for SPA navigation - Expand content script to load on all GitHub repository pages - Add debouncing (500ms) to prevent infinite MutationObserver loops - Implement processing flag to prevent concurrent API calls - Add turbo:load and pjax:end event listeners for SPA routing - Wait for DOMContentLoaded when page is still loading fix #18 --- extension/public/manifest.json | 2 +- extension/src/content.ts | 101 ++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/extension/public/manifest.json b/extension/public/manifest.json index e0ff05c..2b0414b 100644 --- a/extension/public/manifest.json +++ b/extension/public/manifest.json @@ -27,7 +27,7 @@ }, "content_scripts": [ { - "matches": ["https://github.com/*/*/issues*"], + "matches": ["https://github.com/*/*"], "js": ["content.js"], "css": ["styles.css"], "run_at": "document_end" diff --git a/extension/src/content.ts b/extension/src/content.ts index 087c55c..f5d01c9 100644 --- a/extension/src/content.ts +++ b/extension/src/content.ts @@ -17,15 +17,23 @@ statuses?: IssueStatus[]; }; - const GITHUB_ISSUES_URL_PATTERN = - /https:\/\/github\.com\/[^/]+\/[^/]+\/issues/; const BADGE_CLASS = "project-status-badge"; + const DEBOUNCE_DELAY_MS = 500; const DEFAULT_BADGE_COLOR = "#6e7781"; + const GITHUB_ISSUES_URL_PATTERN = + /https:\/\/github\.com\/[^/]+\/[^/]+\/issues/; + const ISSUE_LINK_SELECTOR = '[data-testid="issue-pr-title-link"]'; + const POLL_INTERVAL_MS = 200; + const POLL_TIMEOUT_MS = 5000; + + let debounceTimer: ReturnType | null = null; + let isProcessing = false; + let observer: MutationObserver | null = null; + let pollIntervalId: ReturnType | null = null; + let pollTimeoutId: ReturnType | null = null; const getIssueNumbers = (): number[] => { - const issueElements = document.querySelectorAll( - '[data-testid="issue-pr-title-link"]' - ); + const issueElements = document.querySelectorAll(ISSUE_LINK_SELECTOR); const numbers: number[] = []; @@ -73,9 +81,7 @@ status: string, color: string | null ) => { - const issueLinks = document.querySelectorAll( - '[data-testid="issue-pr-title-link"]' - ); + const issueLinks = document.querySelectorAll(ISSUE_LINK_SELECTOR); for (const link of Array.from(issueLinks)) { const href = link.getAttribute("href"); @@ -116,6 +122,10 @@ const issueNumbers = getIssueNumbers(); if (issueNumbers.length === 0) return; + if (isProcessing) return; + + isProcessing = true; + try { const request: MessageRequest = { issueNumbers, @@ -143,16 +153,22 @@ }); } catch (error) { // Silent fail + } finally { + isProcessing = false; } }; - const init = () => { - if (!window.location.href.match(GITHUB_ISSUES_URL_PATTERN)) return; + const startObserving = () => { + if (observer) return; - updateIssueStatuses(); + observer = new MutationObserver(() => { + if (debounceTimer) { + clearTimeout(debounceTimer); + } - const observer = new MutationObserver(() => { - updateIssueStatuses(); + debounceTimer = setTimeout(() => { + updateIssueStatuses(); + }, DEBOUNCE_DELAY_MS); }); observer.observe(document.body, { @@ -161,5 +177,62 @@ }); }; - init(); + const run = () => { + updateIssueStatuses(); + startObserving(); + }; + + const init = () => { + if (!window.location.href.match(GITHUB_ISSUES_URL_PATTERN)) return; + + run(); + }; + + const cleanupPollingTimers = () => { + if (pollIntervalId) { + clearInterval(pollIntervalId); + pollIntervalId = null; + } + if (pollTimeoutId) { + clearTimeout(pollTimeoutId); + pollTimeoutId = null; + } + }; + + const cleanupObserver = () => { + if (observer) { + observer.disconnect(); + observer = null; + } + + if (debounceTimer) { + clearTimeout(debounceTimer); + debounceTimer = null; + } + }; + + const handleSPARouting = () => { + cleanupObserver(); + cleanupPollingTimers(); + + if (!window.location.href.match(GITHUB_ISSUES_URL_PATTERN)) return; + + pollIntervalId = setInterval(() => { + if (document.querySelector(ISSUE_LINK_SELECTOR)) { + cleanupPollingTimers(); + run(); + } + }, POLL_INTERVAL_MS); + + pollTimeoutId = setTimeout(cleanupPollingTimers, POLL_TIMEOUT_MS); + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + + document.addEventListener("turbo:load", handleSPARouting); + document.addEventListener("pjax:end", handleSPARouting); })();