refactor(dock): implement PollingManager for unread count updates, fixed #3891

- Removed redundant polling logic from DockService and replaced it with a PollingManager class to handle polling operations more efficiently.
- Updated the setting-sync provider to utilize the new polling mechanism for managing dock badge updates.
- Cleaned up unused setDockBadge method from SettingService.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-12 21:20:46 +08:00
parent 20d8590c58
commit 70255af458
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
4 changed files with 66 additions and 45 deletions

View File

@ -1,36 +1,78 @@
import { UNREAD_BACKGROUND_POLLING_INTERVAL } from "../../constants/app"
import { apiClient } from "../../lib/api-client"
import { setDockCount } from "../../lib/dock"
import { sleep } from "../../lib/utils"
import type { IpcContext } from "../base"
import { IpcMethod, IpcService } from "../base"
const pollingMap = {
unread: false,
class PollingManager {
private abortController: AbortController | null = null
private isPolling = false
async startPolling(pollingFn: () => Promise<void>, interval: number): Promise<void> {
if (this.isPolling) {
return // Already polling, prevent duplicate instances
}
this.isPolling = true
this.abortController = new AbortController()
try {
while (!this.abortController.signal.aborted) {
await pollingFn()
// Use AbortSignal with sleep for proper cancellation
await this.sleepWithAbortSignal(interval, this.abortController.signal)
}
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
console.error("Polling error:", error)
}
} finally {
this.isPolling = false
this.abortController = null
}
}
stopPolling(): void {
if (this.abortController) {
this.abortController.abort()
}
}
get active(): boolean {
return this.isPolling
}
private async sleepWithAbortSignal(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, ms)
signal.addEventListener("abort", () => {
clearTimeout(timeoutId)
reject(new DOMException("Aborted", "AbortError"))
})
})
}
}
export class DockService extends IpcService {
private unreadPollingManager = new PollingManager()
constructor() {
super("dock")
}
@IpcMethod()
async pollingUpdateUnreadCount(): Promise<void> {
if (pollingMap.unread) {
return
}
pollingMap.unread = true
while (pollingMap.unread) {
await sleep(UNREAD_BACKGROUND_POLLING_INTERVAL)
if (pollingMap.unread) {
await this.updateUnreadCount()
}
}
await this.unreadPollingManager.startPolling(
() => this.updateUnreadCount(),
UNREAD_BACKGROUND_POLLING_INTERVAL,
)
}
@IpcMethod()
async cancelPollingUpdateUnreadCount(): Promise<void> {
pollingMap.unread = false
this.unreadPollingManager.stopPolling()
}
@IpcMethod()
@ -38,4 +80,9 @@ export class DockService extends IpcService {
const res = await apiClient.reads["total-count"].$get()
setDockCount(res.data.count)
}
@IpcMethod()
setDockBadge(_context: IpcContext, count: number): void {
setDockCount(count)
}
}

View File

@ -2,7 +2,6 @@ import { createRequire } from "node:module"
import { app, nativeTheme } from "electron"
import { setDockCount } from "../../lib/dock"
import { setProxyConfig, updateProxy } from "../../lib/proxy"
import { store } from "../../lib/store"
import { getTrayConfig, setTrayConfig } from "../../lib/tray"
@ -65,11 +64,6 @@ export class SettingService extends IpcService {
setTrayConfig(minimize)
}
@IpcMethod()
setDockBadge(_context: IpcContext, count: number): void {
setDockCount(count)
}
@IpcMethod()
getProxyConfig(_context: IpcContext) {
const proxy = store.get("proxy")

View File

@ -12,7 +12,6 @@ import type { Event } from "electron/main"
import { START_IN_TRAY_ARGS } from "./constants/app"
import { isMacOS, isWindows, isWindows11 } from "./env"
import { filePathToAppUrl, getIconPath } from "./helper"
import { services } from "./ipc"
import { t } from "./lib/i18n"
import { store } from "./lib/store"
import { getTrayConfig } from "./lib/tray"
@ -312,23 +311,6 @@ export const createMainWindow = () => {
}
})
window.on("show", () => {
services.dock.pollingUpdateUnreadCount()
const caller = callWindowExpose(window)
caller.onWindowShow()
})
window.on("hide", async () => {
const caller = callWindowExpose(window)
const settings = await caller.getUISettings()
if (settings?.showDockBadge) {
services.dock.pollingUpdateUnreadCount()
}
})
return window
}

View File

@ -1,5 +1,4 @@
import { isMobile } from "@follow/components/hooks/useMobile.js"
import { unreadActions } from "@follow/store/unread/store"
import i18next from "i18next"
import { useEffect, useInsertionEffect, useLayoutEffect } from "react"
@ -38,12 +37,11 @@ const useUISettingSync = () => {
useEffect(() => {
if (setting.showDockBadge) {
return unreadActions.subscribeUnreadCount(
(count) => ipcServices?.setting.setDockBadge(count),
true,
)
ipcServices?.dock.pollingUpdateUnreadCount()
} else {
ipcServices?.setting.setDockBadge(0)
ipcServices?.dock.cancelPollingUpdateUnreadCount().then(() => {
ipcServices?.dock.setDockBadge(0)
})
}
return
}, [setting.showDockBadge])