From 1e682a26eb4efb344c6c9af4052caefad88a3c3f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Jul 2026 20:39:03 +0800 Subject: [PATCH] refactor: introduce ActivePageTimer to help to do partial page refresh (#38372) Before, the logic is already there for "pull merge box". After, the logic is extracted into a general class ActivePageTimer and will help more pages (including #38329) --- web_src/js/features/common-fetch-action.ts | 17 ++--- web_src/js/features/repo-issue-pull.ts | 56 +++++------------ web_src/js/utils/dom.ts | 72 ++++++++++++++++++++++ 3 files changed, 93 insertions(+), 52 deletions(-) diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index b58aa7a9a0d..8a33fbea084 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -1,6 +1,6 @@ import {GET, request} from '../modules/fetch.ts'; import {hideToastsAll, showErrorToast} from '../modules/toast.ts'; -import {addDelegatedEventListener, createElementFromHTML} from '../utils/dom.ts'; +import {activePageTimerRefresh, addDelegatedEventListener, createElementFromHTML} from '../utils/dom.ts'; import {errorMessage, errorName} from '../modules/errors.ts'; import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; @@ -330,17 +330,10 @@ function initFetchActionTriggerEvery(el: HTMLElement, trigger: string) { const num = parseInt(match[1], 10), unit = match[2]; const intervalMs = unit === 's' ? num * 1000 : num; - const fn = async () => { - try { - await performFetchActionTrigger(el, 'every'); - } finally { - // only continue if the element is still in the document - if (document.contains(el)) { - setTimeout(fn, intervalMs); - } - } - }; - setTimeout(fn, intervalMs); + activePageTimerRefresh({ + interval: () => document.contains(el) ? intervalMs : 0, // only continue if the element is still in the document + async callback() { await performFetchActionTrigger(el, 'every') }, + }); } function initFetchActionTrigger(el: HTMLElement) { diff --git a/web_src/js/features/repo-issue-pull.ts b/web_src/js/features/repo-issue-pull.ts index 7a72d267fce..cecc39ee30b 100644 --- a/web_src/js/features/repo-issue-pull.ts +++ b/web_src/js/features/repo-issue-pull.ts @@ -1,7 +1,7 @@ import {createApp} from 'vue'; import {GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; -import {createElementFromHTML} from '../utils/dom.ts'; +import {createElementFromHTML, activePageTimerRefresh} from '../utils/dom.ts'; import {registerGlobalEventFunc} from '../modules/observer.ts'; export function initRepoPullRequestUpdate(el: HTMLElement) { @@ -35,46 +35,22 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) { view.mount(el); // TODO: can unmount when reloaded? } +function initRepoPullMergeBoxRefresh(el: Element) { + activePageTimerRefresh({ + once: true, // on successful refresh, the data-global-init will re-initialize the element + interval: () => Number(el.getAttribute('data-pull-merge-box-reloading-interval')), + async callback() { + const pullLink = el.getAttribute('data-pull-link')!; + const resp = await GET(`${pullLink}/merge_box`); + if (!resp.ok) return; + const newEl = createElementFromHTML(await resp.text()); + el.replaceWith(newEl); // don't morph, do full replacement to make sure data-global-init and Vue components are re-initialized + }, + }); +} + export function initRepoPullMergeBox(el: HTMLElement) { registerGlobalEventFunc('click', 'onCommitStatusChecksToggle', onCommitStatusChecksToggle); initRepoPullRequestMergeForm(el); - - const reloadingIntervalValue = el.getAttribute('data-pull-merge-box-reloading-interval'); - if (!reloadingIntervalValue) return; - - const reloadingInterval = parseInt(reloadingIntervalValue); - const pullLink = el.getAttribute('data-pull-link'); - let timerId: number | null; - - let reloadMergeBox: () => Promise; - const stopReloading = () => { - if (!timerId) return; - clearTimeout(timerId); - timerId = null; - }; - const startReloading = () => { - if (timerId) return; - setTimeout(reloadMergeBox, reloadingInterval); - }; - const onVisibilityChange = () => { - if (document.hidden) { - stopReloading(); - } else { - startReloading(); - } - }; - reloadMergeBox = async () => { - const resp = await GET(`${pullLink}/merge_box`); - stopReloading(); - if (!resp.ok) { - startReloading(); - return; - } - document.removeEventListener('visibilitychange', onVisibilityChange); - const newElem = createElementFromHTML(await resp.text()); - el.replaceWith(newElem); - }; - - document.addEventListener('visibilitychange', onVisibilityChange); - startReloading(); + initRepoPullMergeBoxRefresh(el); } diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 03ab7831427..010a96eee9d 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -339,3 +339,75 @@ let elemIdCounter = 0; export function generateElemId(prefix: string = ''): string { return `${prefix}${elemIdCounter++}`; } + +export type ActivePageTimerOptions = { + once?: boolean; + interval: () => number; // if it returns 0, the timer is stopped + callback: () => Promise; +}; + +export class ActivePageTimer { + private readonly opts: ActivePageTimerOptions; + private readonly onVisibilityChange: () => void; + + private sysTimerId?: number; + private startTime?: number; + + constructor(opts: ActivePageTimerOptions) { + this.opts = opts; + this.onVisibilityChange = () => { + if (!this.startTime) return; + const interval = this.opts.interval(); + if (document.hidden) { + this.clearSysTimer(); + } else if (interval) { + this.startSysTimer(interval); + } + }; + } + + private async handler() { + this.clear(); + await this.opts.callback(); + if (!this.opts.once) this.start(); + } + + start() { + const interval = this.opts.interval(); + if (!interval) return; + if (this.sysTimerId) return; + if (!this.startTime) { + this.startTime = Date.now(); + document.addEventListener('visibilitychange', this.onVisibilityChange); + } + if (!document.hidden) this.startSysTimer(interval); + } + + private startSysTimer(interval: number) { + const remaining = interval - (Date.now() - this.startTime!); + if (remaining <= 0) { + this.handler(); + } else { + this.sysTimerId = window.setTimeout(() => this.handler(), remaining); + } + } + + private clearSysTimer() { + if (!this.sysTimerId) return; + window.clearTimeout(this.sysTimerId); + this.sysTimerId = undefined; + } + + clear() { + if (!this.startTime) return; + this.startTime = undefined; + this.clearSysTimer(); + document.removeEventListener('visibilitychange', this.onVisibilityChange); + } +} + +export function activePageTimerRefresh(opts: ActivePageTimerOptions): ActivePageTimer { + const timer = new ActivePageTimer(opts); + timer.start(); + return timer; +}