perf(admin-web): reduce redundant polling and cover active publish status
Unify list/detail poll intervals to 5s and skip refetch while a query is already in flight, replacing broad invalidateQueries cascades with targeted refetches. Poll now also triggers on active publish status via the new hasActivePublishStatus helper. Dedupe concurrent refreshBrands calls with a shared promise and skip the redundant AppShell refresh when already initialized. Gate publish-record loading on popover open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { CopyOutlined, LinkOutlined } from '@ant-design/icons-vue'
|
||||
import type { ArticleVersion, PublishRecord } from '@geo/shared-types'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import type { TableColumnsType } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getPublishStatusMeta,
|
||||
getSourceTypeLabel,
|
||||
hasActiveGenerationStatus,
|
||||
hasActivePublishStatus,
|
||||
} from '@/lib/display'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
@@ -34,7 +35,6 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const activeTab = ref('content')
|
||||
@@ -43,6 +43,7 @@ const streamTitle = ref('')
|
||||
const streamMarkdown = ref('')
|
||||
const streamStatus = ref<string | null>(null)
|
||||
const streamFeatureEnabled = isGenerationStreamEnabled()
|
||||
const ARTICLE_DETAIL_POLL_INTERVAL_MS = 5000
|
||||
|
||||
const detailQuery = useQuery({
|
||||
queryKey: computed(() => ['articles', 'detail', companyStore.currentBrandId, props.articleId]),
|
||||
@@ -178,18 +179,30 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => props.open, () => props.articleId, () => detail.value?.generate_status],
|
||||
([open, articleId, generateStatus], _prev, onCleanup) => {
|
||||
if (!open || !articleId || !hasActiveGenerationStatus(generateStatus)) {
|
||||
[
|
||||
() => props.open,
|
||||
() => props.articleId,
|
||||
() => detail.value?.generate_status,
|
||||
() => detail.value?.publish_status,
|
||||
],
|
||||
([open, articleId, generateStatus, publishStatus], _prev, onCleanup) => {
|
||||
const shouldPoll =
|
||||
hasActiveGenerationStatus(generateStatus) || hasActivePublishStatus(publishStatus)
|
||||
|
||||
if (!open || !articleId || !shouldPoll) {
|
||||
return
|
||||
}
|
||||
|
||||
const timer = window.setInterval(
|
||||
() => {
|
||||
void refreshArticleData()
|
||||
},
|
||||
streamFeatureEnabled ? 5000 : 2000,
|
||||
)
|
||||
const timer = window.setInterval(() => {
|
||||
if (
|
||||
detailQuery.isFetching.value ||
|
||||
versionsQuery.isFetching.value ||
|
||||
publishRecordsQuery.isFetching.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
void refreshArticleData()
|
||||
}, ARTICLE_DETAIL_POLL_INTERVAL_MS)
|
||||
|
||||
onCleanup(() => {
|
||||
window.clearInterval(timer)
|
||||
@@ -234,13 +247,7 @@ function resetStreamState(): void {
|
||||
}
|
||||
|
||||
async function refreshArticleData(): Promise<void> {
|
||||
await Promise.all([
|
||||
detailQuery.refetch(),
|
||||
versionsQuery.refetch(),
|
||||
publishRecordsQuery.refetch(),
|
||||
queryClient.invalidateQueries({ queryKey: ['articles'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
|
||||
])
|
||||
await Promise.all([detailQuery.refetch(), versionsQuery.refetch(), publishRecordsQuery.refetch()])
|
||||
}
|
||||
|
||||
function resolvePublishLink(record: PublishRecord): string {
|
||||
|
||||
@@ -63,7 +63,9 @@ const publishRecordsQuery = useQuery({
|
||||
companyStore.currentBrandId,
|
||||
props.articleId,
|
||||
]),
|
||||
enabled: computed(() => shouldLoadRecords.value && Boolean(companyStore.currentBrandId)),
|
||||
enabled: computed(
|
||||
() => popoverOpen.value && shouldLoadRecords.value && Boolean(companyStore.currentBrandId),
|
||||
),
|
||||
queryFn: () => articlesApi.publishRecords(props.articleId as number),
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
|
||||
@@ -17,7 +17,12 @@ import { articlesApi, promptRulesApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { copyTextToClipboard } from '@/lib/clipboard'
|
||||
import { getGenerateStatusMeta, getPublishStatusMeta } from '@/lib/display'
|
||||
import {
|
||||
getGenerateStatusMeta,
|
||||
getPublishStatusMeta,
|
||||
hasActiveGenerationStatus,
|
||||
hasActivePublishStatus,
|
||||
} from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { formatPublishPlatformList } from '@/lib/publish-platforms'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
@@ -109,6 +114,7 @@ const listQuery = useQuery({
|
||||
})
|
||||
|
||||
let pollingTimer: number | null = null
|
||||
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => articlesApi.remove(id),
|
||||
@@ -205,8 +211,10 @@ function handleTableChange(nextPage: number, nextPageSize: number): void {
|
||||
function startPolling(): void {
|
||||
if (pollingTimer !== null) return
|
||||
pollingTimer = window.setInterval(() => {
|
||||
void listQuery.refetch()
|
||||
}, 3000)
|
||||
if (!listQuery.isFetching.value) {
|
||||
void listQuery.refetch()
|
||||
}
|
||||
}, ARTICLE_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling(): void {
|
||||
@@ -236,7 +244,9 @@ watch(
|
||||
() => listQuery.data.value?.items ?? [],
|
||||
(items) => {
|
||||
const hasActive = items.some(
|
||||
(item) => item.generate_status === 'generating' || item.generate_status === 'running',
|
||||
(item) =>
|
||||
hasActiveGenerationStatus(item.generate_status) ||
|
||||
hasActivePublishStatus(item.publish_status),
|
||||
)
|
||||
if (hasActive) {
|
||||
startPolling()
|
||||
|
||||
@@ -117,6 +117,7 @@ const columns = computed<TableColumnsType<InstantTaskItem>>(() => [
|
||||
])
|
||||
|
||||
let pollingTimer: number | null = null
|
||||
const TASK_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
function applyFilters(): void {
|
||||
page.value = 1
|
||||
@@ -153,8 +154,10 @@ function openTaskArticles(task: InstantTaskItem): void {
|
||||
function startPolling(): void {
|
||||
if (pollingTimer !== null) return
|
||||
pollingTimer = window.setInterval(() => {
|
||||
void listQuery.refetch()
|
||||
}, 3000)
|
||||
if (!listQuery.isFetching.value) {
|
||||
void listQuery.refetch()
|
||||
}
|
||||
}, TASK_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling(): void {
|
||||
|
||||
@@ -235,12 +235,15 @@ function openTaskArticles(task: ScheduleTask): void {
|
||||
}
|
||||
|
||||
let pollingTimer: number | null = null
|
||||
const TASK_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
function startPolling(): void {
|
||||
if (pollingTimer !== null) return
|
||||
pollingTimer = window.setInterval(() => {
|
||||
void listQuery.refetch()
|
||||
}, 3000)
|
||||
if (!listQuery.isFetching.value) {
|
||||
void listQuery.refetch()
|
||||
}
|
||||
}, TASK_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling(): void {
|
||||
|
||||
@@ -518,7 +518,7 @@ onMounted(() => {
|
||||
syncMobileState(mobileMediaQuery)
|
||||
mobileMediaQuery.addEventListener('change', syncMobileState)
|
||||
|
||||
if (!isMembershipBlocked.value) {
|
||||
if (!isMembershipBlocked.value && !companyStore.initialized) {
|
||||
void companyStore.refreshBrands()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -89,6 +89,18 @@ export function hasActiveGenerationStatus(status?: string | null): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export function hasActivePublishStatus(status?: string | null): boolean {
|
||||
const normalized = String(status ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return (
|
||||
normalized === 'queued' ||
|
||||
normalized === 'pending' ||
|
||||
normalized === 'publishing' ||
|
||||
normalized === 'running'
|
||||
)
|
||||
}
|
||||
|
||||
export function getPublishStatusMeta(status?: string | null): { label: string; color: string } {
|
||||
if (!status) {
|
||||
return { label: '--', color: 'default' }
|
||||
|
||||
@@ -15,6 +15,7 @@ export const useCompanyStore = defineStore('company', () => {
|
||||
const currentBrandId = ref<number | null>(readStoredCurrentBrandId())
|
||||
const loading = ref(false)
|
||||
const initialized = ref(false)
|
||||
let refreshPromise: Promise<Brand[]> | null = null
|
||||
|
||||
const currentBrand = computed(
|
||||
() => brands.value.find((brand) => brand.id === currentBrandId.value) ?? null,
|
||||
@@ -56,15 +57,25 @@ export const useCompanyStore = defineStore('company', () => {
|
||||
}
|
||||
|
||||
async function refreshBrands(): Promise<Brand[]> {
|
||||
loading.value = true
|
||||
try {
|
||||
const nextBrands = normalizeBrandList(await brandsApi.list())
|
||||
setBrands(nextBrands)
|
||||
initialized.value = true
|
||||
return nextBrands
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (refreshPromise) {
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
refreshPromise = brandsApi
|
||||
.list()
|
||||
.then((value) => {
|
||||
const nextBrands = normalizeBrandList(value)
|
||||
setBrands(nextBrands)
|
||||
initialized.value = true
|
||||
return nextBrands
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshPromise = null
|
||||
})
|
||||
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
function setBrands(value: Brand[] | null | undefined): void {
|
||||
@@ -78,6 +89,7 @@ export const useCompanyStore = defineStore('company', () => {
|
||||
currentBrandId.value = null
|
||||
initialized.value = false
|
||||
loading.value = false
|
||||
refreshPromise = null
|
||||
clearStoredCurrentBrandId()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { TableColumnsType } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -18,7 +18,7 @@ import { articlesApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { copyTextToClipboard } from '@/lib/clipboard'
|
||||
import { getPublishStatusMeta } from '@/lib/display'
|
||||
import { getPublishStatusMeta, hasActivePublishStatus } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
@@ -34,6 +34,8 @@ const selectedArticleId = ref<number | null>(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const createdRange = ref<[Dayjs, Dayjs] | null>(null)
|
||||
let articlePollingTimer: number | null = null
|
||||
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
const draftFilters = reactive<{
|
||||
publish_status?: string
|
||||
@@ -172,11 +174,51 @@ function openPreview(article: ArticleListItem): void {
|
||||
articleDrawerOpen.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => articleListQuery.data.value?.items || [],
|
||||
(items: ArticleListItem[]) => {
|
||||
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
|
||||
|
||||
if (hasPublishingArticles) {
|
||||
startArticlePolling()
|
||||
return
|
||||
}
|
||||
|
||||
stopArticlePolling()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopArticlePolling()
|
||||
})
|
||||
|
||||
function handleTableChange(nextPage: number, nextPageSize: number): void {
|
||||
page.value = nextPage
|
||||
pageSize.value = nextPageSize
|
||||
}
|
||||
|
||||
function startArticlePolling(): void {
|
||||
if (articlePollingTimer !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
articlePollingTimer = window.setInterval(() => {
|
||||
if (!articleListQuery.isFetching.value) {
|
||||
void articleListQuery.refetch()
|
||||
}
|
||||
}, ARTICLE_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopArticlePolling(): void {
|
||||
if (articlePollingTimer === null) {
|
||||
return
|
||||
}
|
||||
|
||||
window.clearInterval(articlePollingTimer)
|
||||
articlePollingTimer = null
|
||||
}
|
||||
|
||||
async function handleDelete(articleId: number): Promise<void> {
|
||||
await deleteMutation.mutateAsync(articleId)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getGenerateStatusMeta,
|
||||
getPublishStatusMeta,
|
||||
hasActiveGenerationStatus,
|
||||
hasActivePublishStatus,
|
||||
} from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
@@ -93,6 +94,7 @@ const articleListQuery = useQuery({
|
||||
})
|
||||
|
||||
let articlePollingTimer: number | null = null
|
||||
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (articleId: number) => articlesApi.remove(articleId),
|
||||
@@ -180,8 +182,9 @@ watch(
|
||||
const hasGeneratingArticles = items.some((item) =>
|
||||
hasActiveGenerationStatus(item.generate_status),
|
||||
)
|
||||
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
|
||||
|
||||
if (hasGeneratingArticles) {
|
||||
if (hasGeneratingArticles || hasPublishingArticles) {
|
||||
startArticlePolling()
|
||||
return
|
||||
}
|
||||
@@ -245,9 +248,10 @@ function startArticlePolling(): void {
|
||||
}
|
||||
|
||||
articlePollingTimer = window.setInterval(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['articles'] })
|
||||
void articleListQuery.refetch()
|
||||
}, 3000)
|
||||
if (!articleListQuery.isFetching.value) {
|
||||
void articleListQuery.refetch()
|
||||
}
|
||||
}, ARTICLE_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopArticlePolling(): void {
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
getPublishStatusMeta,
|
||||
getTemplateMeta,
|
||||
hasActiveGenerationStatus,
|
||||
hasActivePublishStatus,
|
||||
} from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
@@ -207,6 +208,7 @@ const displayedArticleTotal = computed(() =>
|
||||
)
|
||||
|
||||
let articlePollingTimer: number | null = null
|
||||
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (articleId: number) => articlesApi.remove(articleId),
|
||||
@@ -418,8 +420,11 @@ watch(
|
||||
const hasGeneratingArticles = items.some((item: ArticleListItem) =>
|
||||
hasActiveGenerationStatus(item.generate_status),
|
||||
)
|
||||
const hasPublishingArticles = items.some((item: ArticleListItem) =>
|
||||
hasActivePublishStatus(item.publish_status),
|
||||
)
|
||||
|
||||
if (hasGeneratingArticles) {
|
||||
if (hasGeneratingArticles || hasPublishingArticles) {
|
||||
startArticlePolling()
|
||||
return
|
||||
}
|
||||
@@ -444,10 +449,17 @@ function startArticlePolling(): void {
|
||||
}
|
||||
|
||||
articlePollingTimer = window.setInterval(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['articles'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['workspace', 'recent-articles'] })
|
||||
void Promise.all([articleListQuery.refetch(), recentArticlesQuery.refetch()])
|
||||
}, 3000)
|
||||
if (shouldUseRecentFallback.value) {
|
||||
if (!recentArticlesQuery.isFetching.value) {
|
||||
void recentArticlesQuery.refetch()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!articleListQuery.isFetching.value) {
|
||||
void articleListQuery.refetch()
|
||||
}
|
||||
}, ARTICLE_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopArticlePolling(): void {
|
||||
|
||||
@@ -31,7 +31,12 @@ import { articlesApi, tenantAccountsApi, workspaceApi } from '@/lib/api'
|
||||
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
|
||||
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
|
||||
import { copyTextToClipboard } from '@/lib/clipboard'
|
||||
import { formatDateTime, getTemplateMeta, hasActiveGenerationStatus } from '@/lib/display'
|
||||
import {
|
||||
formatDateTime,
|
||||
getTemplateMeta,
|
||||
hasActiveGenerationStatus,
|
||||
hasActivePublishStatus,
|
||||
} from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { isMediaPublishAccount } from '@/lib/publish-platforms'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
@@ -45,6 +50,7 @@ const articleDrawerOpen = ref(false)
|
||||
const selectedPublishArticleId = ref<number | null>(null)
|
||||
const publishModalOpen = ref(false)
|
||||
let recentArticlesPollingTimer: number | null = null
|
||||
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
|
||||
|
||||
const overviewQuery = useQuery({
|
||||
queryKey: computed(() => ['workspace', 'overview', companyStore.currentBrandId]),
|
||||
@@ -300,8 +306,9 @@ watch(
|
||||
const hasGeneratingArticles = items.some((item) =>
|
||||
hasActiveGenerationStatus(item.generate_status),
|
||||
)
|
||||
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
|
||||
|
||||
if (hasGeneratingArticles) {
|
||||
if (hasGeneratingArticles || hasPublishingArticles) {
|
||||
startRecentArticlesPolling()
|
||||
return
|
||||
}
|
||||
@@ -325,12 +332,11 @@ function startRecentArticlesPolling(): void {
|
||||
stopRecentArticlesPolling()
|
||||
return
|
||||
}
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['workspace', 'recent-articles', companyStore.currentBrandId],
|
||||
})
|
||||
void queryClient.invalidateQueries({ queryKey: ['articles'] })
|
||||
if (recentArticlesQuery.isFetching.value) {
|
||||
return
|
||||
}
|
||||
void recentArticlesQuery.refetch()
|
||||
}, 3000)
|
||||
}, ARTICLE_LIST_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopRecentArticlesPolling(): void {
|
||||
|
||||
Reference in New Issue
Block a user