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, notePublishTaskLeaseMiss,
selectNextPublishTask, selectNextPublishTask,
} from './publish-scheduler' } from './publish-scheduler'
import { onRuntimeInvalidated } from './runtime-events' import { emitRuntimeInvalidated, onRuntimeInvalidated } from './runtime-events'
import { import {
initScheduler, initScheduler,
noteSchedulerAccountsSync, noteSchedulerAccountsSync,
@@ -2426,6 +2426,7 @@ async function executeLeasedTask(
noteMonitorTaskLeased(task, routing) noteMonitorTaskLeased(task, routing)
} else { } else {
notePublishTaskActivated(taskRecord.id, taskRecord.platform) notePublishTaskActivated(taskRecord.id, taskRecord.platform)
emitRuntimeInvalidated('publish-task-lease', { taskId: taskRecord.id })
} }
syncSchedulerSurface() syncSchedulerSurface()
queueMicrotask(() => pumpExecutionLoop()) queueMicrotask(() => pumpExecutionLoop())
@@ -1,16 +1,17 @@
type RuntimeInvalidationReason = 'account-health' type RuntimeInvalidationReason = 'account-health' | 'publish-task-lease'
type RuntimeInvalidationListener = (event: { type RuntimeInvalidationListener = (event: {
reason: RuntimeInvalidationReason reason: RuntimeInvalidationReason
at: number at: number
accountId?: string accountId?: string
taskId?: string
}) => void }) => void
const listeners = new Set<RuntimeInvalidationListener>() const listeners = new Set<RuntimeInvalidationListener>()
export function emitRuntimeInvalidated( export function emitRuntimeInvalidated(
reason: RuntimeInvalidationReason, reason: RuntimeInvalidationReason,
details: { accountId?: string } = {}, details: { accountId?: string; taskId?: string } = {},
): void { ): void {
const event = { const event = {
reason, reason,
+12 -2
View File
@@ -145,11 +145,21 @@ const desktopBridge = {
setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) => setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) =>
ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise<null>, ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise<null>,
onRuntimeInvalidated: ( 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 = ( const wrapped = (
_event: IpcRendererEvent, _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) listener(payload)
} }
+6 -1
View File
@@ -64,7 +64,12 @@ declare global {
}, },
): Promise<null> ): Promise<null>
onRuntimeInvalidated( 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 ): () => void
onNetworkObserved(listener: (event: DesktopObservedRequest) => void): () => void onNetworkObserved(listener: (event: DesktopObservedRequest) => void): () => void
} }
@@ -26,6 +26,7 @@ import {
const PAGE_SIZE = 10 const PAGE_SIZE = 10
const INLINE_ERROR_HEAD_CHARS = 48 const INLINE_ERROR_HEAD_CHARS = 48
const INLINE_ERROR_TAIL_CHARS = 10 const INLINE_ERROR_TAIL_CHARS = 10
const ACTIVE_TASK_POLL_INTERVAL_MS = 5_000
const publishPlatformIndex: Record<string, (typeof desktopPublishMediaCatalog)[number]> = const publishPlatformIndex: Record<string, (typeof desktopPublishMediaCatalog)[number]> =
Object.fromEntries(desktopPublishMediaCatalog.map((item) => [item.id, item])) Object.fromEntries(desktopPublishMediaCatalog.map((item) => [item.id, item]))
@@ -52,6 +53,7 @@ interface PublishTaskItem {
} }
let refreshTimer: ReturnType<typeof setInterval> | null = null let refreshTimer: ReturnType<typeof setInterval> | null = null
let runtimeInvalidationUnsubscribe: (() => void) | null = null
function platformMeta(platform: string) { function platformMeta(platform: string) {
const key = (platform ?? '').toLowerCase() const key = (platform ?? '').toLowerCase()
@@ -104,6 +106,12 @@ function isPendingStatus(status: DesktopTaskInfo['status']): boolean {
return status === 'queued' || status === 'in_progress' 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' { function statusTone(status: DesktopTaskInfo['status']): 'info' | 'warn' | 'success' | 'danger' {
switch (status) { switch (status) {
case 'queued': case 'queued':
@@ -359,13 +367,61 @@ async function refreshTasks(page = currentPage.value) {
taskPage.value = response taskPage.value = response
currentPage.value = response.page currentPage.value = response.page
syncActiveTaskPolling(response)
} catch (err) { } catch (err) {
error.value = normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '发布任务暂时不可用')) error.value = normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '发布任务暂时不可用'))
if (!hasActivePublishingTask(taskPage.value)) {
stopActiveTaskPolling()
}
} finally { } finally {
loading.value = false 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) { async function retryTask(taskId: string) {
actionPendingTaskId.value = taskId actionPendingTaskId.value = taskId
@@ -480,17 +536,13 @@ watch(searchTitle, (value) => {
}) })
onMounted(() => { onMounted(() => {
bindPublishLeaseListener()
void refreshTasks() void refreshTasks()
refreshTimer = setInterval(() => {
void refreshTasks()
}, 20_000)
}) })
onUnmounted(() => { onUnmounted(() => {
if (refreshTimer) { unbindPublishLeaseListener()
clearInterval(refreshTimer) stopActiveTaskPolling()
refreshTimer = null
}
}) })
const accountNameMap = computed(() => { const accountNameMap = computed(() => {