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:
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user