Files
order_site/apps/frontend/src/composables/tencent/useTencentBrowserSessionPolling.ts
T

199 lines
4.8 KiB
TypeScript

import { computed, ref, type Ref } from 'vue'
import {
fetchTencentBrowserSession,
fetchTencentBrowserSessionSummary,
} from '@/services/tencent/session'
import type {
TencentBrowserSessionData,
TencentBrowserSessionStatus,
TencentBrowserSessionSummaryData,
} from '@/types/tencent/session'
import { resolveTencentActionMessage } from './session-errors'
const POLL_INTERVAL_MS = 2_500
const POLL_FAILURE_LIMIT = 3
export const ACTIVE_TENCENT_SESSION_STATUSES = new Set([
'waiting_scan',
'scanned',
'logged_in',
'ready_to_redeem',
'redeeming',
])
export function useTencentBrowserSessionPolling(options: {
session: Ref<TencentBrowserSessionData | null>
sessionLoading: Ref<boolean>
notifyActionError: (error: unknown, fallbackMessage: string) => void
}) {
const { session, sessionLoading, notifyActionError } = options
const pollFailureCount = ref(0)
const pollWarningMessage = ref('')
let pollTimer: ReturnType<typeof setTimeout> | null = null
let pollToken = 0
const sessionNotice = computed(() => pollWarningMessage.value || session.value?.notice || '')
async function refreshSession({ silent = false } = {}) {
if (!session.value?.sessionId) {
return false
}
if (!silent) {
sessionLoading.value = true
resetPollingWarning()
}
try {
const currentSession = session.value
const response = await fetchTencentBrowserSessionSummary(currentSession.sessionId)
if (response.code !== 0) {
throw new Error(response.msg || '获取浏览器会话状态失败')
}
const nextSession = mergeSessionData(currentSession, response.data)
session.value = nextSession
if (shouldRefreshQrImage(currentSession, response.data)) {
const fullResponse = await fetchTencentBrowserSession(currentSession.sessionId)
if (fullResponse.code !== 0) {
throw new Error(fullResponse.msg || '获取浏览器会话二维码失败')
}
session.value = mergeSessionData(nextSession, fullResponse.data)
}
if (silent) {
resetPollingWarning()
}
return true
} catch (error) {
if (silent) {
handleSilentPollingError(error)
} else {
notifyActionError(error, '获取浏览器会话状态失败')
}
return false
} finally {
if (!silent) {
sessionLoading.value = false
}
}
}
function startPolling() {
const token = ++pollToken
const loop = async () => {
if (token !== pollToken || !session.value?.sessionId) {
return
}
await refreshSession({ silent: true })
if (token !== pollToken || !isActiveTencentSessionStatus(session.value?.status)) {
return
}
pollTimer = setTimeout(() => {
void loop()
}, POLL_INTERVAL_MS)
}
pollTimer = setTimeout(() => {
void loop()
}, POLL_INTERVAL_MS)
}
function resetPolling() {
pollToken += 1
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
}
function resetPollingWarning() {
pollFailureCount.value = 0
pollWarningMessage.value = ''
}
function restartPollingIfActive(nextSession = session.value) {
if (isActiveTencentSessionStatus(nextSession?.status)) {
startPolling()
}
}
function handleSilentPollingError(error: unknown) {
console.error(error)
pollFailureCount.value += 1
if (pollFailureCount.value < POLL_FAILURE_LIMIT) {
return
}
pollWarningMessage.value = `${resolveTencentActionMessage(error, '会话状态刷新失败')},已暂停自动轮询,请手动刷新或重新生成二维码。`
resetPolling()
}
return {
refreshSession,
resetPolling,
resetPollingWarning,
restartPollingIfActive,
sessionNotice,
startPolling,
}
}
export function isActiveTencentSessionStatus(
status: TencentBrowserSessionStatus | null | undefined,
) {
return Boolean(status && ACTIVE_TENCENT_SESSION_STATUSES.has(status))
}
function mergeSessionData(
current: TencentBrowserSessionData | null,
next: TencentBrowserSessionSummaryData | TencentBrowserSessionData,
) {
if (!current) {
return {
...next,
qrImageBase64: next.qrImageBase64 || '',
}
}
return {
...current,
...next,
qrImageBase64:
typeof next.qrImageBase64 === 'string' ? next.qrImageBase64 : current.qrImageBase64,
activityInfo: next.activityInfo ?? current.activityInfo ?? null,
redeem: next.redeem ?? current.redeem ?? null,
artifacts: next.artifacts || current.artifacts,
}
}
function shouldRefreshQrImage(
current: TencentBrowserSessionData,
next: TencentBrowserSessionSummaryData,
) {
if (!next.artifacts?.hasQrImage) {
return false
}
if (!current.qrImageBase64) {
return true
}
return Boolean(next.qrUpdatedAt && next.qrUpdatedAt !== current.qrUpdatedAt)
}