feat(desktop): refresh publish tasks instantly on lease activation

Emit a 'publish-task-lease' runtime invalidation event whenever the
scheduler activates a publish task, and have PublishManagementView
listen for it to trigger an immediate refresh. The view also switches
to a 5s active-task poll only while a task is in_progress, replacing
the unconditional 20s timer.
This commit is contained in:
2026-05-06 21:34:13 +08:00
parent ddce8b3d8d
commit df467b63b4
5 changed files with 82 additions and 13 deletions
@@ -84,7 +84,7 @@ import {
notePublishTaskLeaseMiss,
selectNextPublishTask,
} from './publish-scheduler'
import { onRuntimeInvalidated } from './runtime-events'
import { emitRuntimeInvalidated, onRuntimeInvalidated } from './runtime-events'
import {
initScheduler,
noteSchedulerAccountsSync,
@@ -2426,6 +2426,7 @@ async function executeLeasedTask(
noteMonitorTaskLeased(task, routing)
} else {
notePublishTaskActivated(taskRecord.id, taskRecord.platform)
emitRuntimeInvalidated('publish-task-lease', { taskId: taskRecord.id })
}
syncSchedulerSurface()
queueMicrotask(() => pumpExecutionLoop())
@@ -1,16 +1,17 @@
type RuntimeInvalidationReason = 'account-health'
type RuntimeInvalidationReason = 'account-health' | 'publish-task-lease'
type RuntimeInvalidationListener = (event: {
reason: RuntimeInvalidationReason
at: number
accountId?: string
taskId?: string
}) => void
const listeners = new Set<RuntimeInvalidationListener>()
export function emitRuntimeInvalidated(
reason: RuntimeInvalidationReason,
details: { accountId?: string } = {},
details: { accountId?: string; taskId?: string } = {},
): void {
const event = {
reason,
+12 -2
View File
@@ -145,11 +145,21 @@ const desktopBridge = {
setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) =>
ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise<null>,
onRuntimeInvalidated: (
listener: (event: { reason: 'account-health'; at: number; accountId?: string }) => void,
listener: (event: {
reason: 'account-health' | 'publish-task-lease'
at: number
accountId?: string
taskId?: string
}) => void,
) => {
const wrapped = (
_event: IpcRendererEvent,
payload: { reason: 'account-health'; at: number; accountId?: string },
payload: {
reason: 'account-health' | 'publish-task-lease'
at: number
accountId?: string
taskId?: string
},
) => {
listener(payload)
}
+6 -1
View File
@@ -64,7 +64,12 @@ declare global {
},
): Promise<null>
onRuntimeInvalidated(
listener: (event: { reason: 'account-health'; at: number; accountId?: string }) => void,
listener: (event: {
reason: 'account-health' | 'publish-task-lease'
at: number
accountId?: string
taskId?: string
}) => void,
): () => void
onNetworkObserved(listener: (event: DesktopObservedRequest) => void): () => void
}
@@ -26,6 +26,7 @@ import {
const PAGE_SIZE = 10
const INLINE_ERROR_HEAD_CHARS = 48
const INLINE_ERROR_TAIL_CHARS = 10
const ACTIVE_TASK_POLL_INTERVAL_MS = 5_000
const publishPlatformIndex: Record<string, (typeof desktopPublishMediaCatalog)[number]> =
Object.fromEntries(desktopPublishMediaCatalog.map((item) => [item.id, item]))
@@ -52,6 +53,7 @@ interface PublishTaskItem {
}
let refreshTimer: ReturnType<typeof setInterval> | null = null
let runtimeInvalidationUnsubscribe: (() => void) | null = null
function platformMeta(platform: string) {
const key = (platform ?? '').toLowerCase()
@@ -104,6 +106,12 @@ function isPendingStatus(status: DesktopTaskInfo['status']): boolean {
return status === 'queued' || status === 'in_progress'
}
function hasActivePublishingTask(page: DesktopPublishTaskListResponse | null): boolean {
return Boolean(
page?.items.some((task) => normalizePublishTaskStatus(task.status) === 'in_progress'),
)
}
function statusTone(status: DesktopTaskInfo['status']): 'info' | 'warn' | 'success' | 'danger' {
switch (status) {
case 'queued':
@@ -359,13 +367,61 @@ async function refreshTasks(page = currentPage.value) {
taskPage.value = response
currentPage.value = response.page
syncActiveTaskPolling(response)
} catch (err) {
error.value = normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '发布任务暂时不可用'))
if (!hasActivePublishingTask(taskPage.value)) {
stopActiveTaskPolling()
}
} finally {
loading.value = false
}
}
function startActiveTaskPolling() {
if (refreshTimer) {
return
}
refreshTimer = setInterval(() => {
void refreshTasks()
}, ACTIVE_TASK_POLL_INTERVAL_MS)
}
function stopActiveTaskPolling() {
if (!refreshTimer) {
return
}
clearInterval(refreshTimer)
refreshTimer = null
}
function syncActiveTaskPolling(page = taskPage.value) {
if (hasActivePublishingTask(page)) {
startActiveTaskPolling()
return
}
stopActiveTaskPolling()
}
function bindPublishLeaseListener() {
if (runtimeInvalidationUnsubscribe || !window.desktopBridge?.app?.onRuntimeInvalidated) {
return
}
runtimeInvalidationUnsubscribe = window.desktopBridge.app.onRuntimeInvalidated((event) => {
if (event.reason !== 'publish-task-lease') {
return
}
void refreshTasks(1)
})
}
function unbindPublishLeaseListener() {
runtimeInvalidationUnsubscribe?.()
runtimeInvalidationUnsubscribe = null
}
async function retryTask(taskId: string) {
actionPendingTaskId.value = taskId
@@ -480,17 +536,13 @@ watch(searchTitle, (value) => {
})
onMounted(() => {
bindPublishLeaseListener()
void refreshTasks()
refreshTimer = setInterval(() => {
void refreshTasks()
}, 20_000)
})
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
unbindPublishLeaseListener()
stopActiveTaskPolling()
})
const accountNameMap = computed(() => {